Better SteamPYPlus

Better SteamPY基础上增加了绝版游戏筛选、自动翻页、慈善包筛选功能

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

Advertisement:

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

Advertisement:

// ==UserScript==
// @name         Better SteamPYPlus
// @namespace    https://space.bilibili.com/93654843
// @version      1.0.3
// @description  Better SteamPY基础上增加了绝版游戏筛选、自动翻页、慈善包筛选功能
// @author       FiNNiER、ZY
// @match        *://steampy.com/*
// @icon         https://steampy.com/img/logo.63413a4f.png
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @connect      api.steampowered.com
// @connect      store.steampowered.com
// @connect      steam-tracker.com
// @connect      barter.vg
// @connect      bartervg.com
// @run-at       document-body
// ==/UserScript==
var Saves = {
  wishlist: [],
  ownedApps: [],
  familygameList: [],
  lastupdatetime: 0,
};
var limitedApps = [];
var limitedAppsSet = new Set();
var noGameList = [];
var noGameListSet = new Set();
var delistedGamesData = null; // 存储下架游戏数据
var noDlc = false;
var noownedGames = false;
var noRestrictedGames = false;
var onlyDelistedGames = false; // 只显示下架游戏
var delistedTypes = []; // 选中的下架类型
var autoSkipTimer = null; // 用于防抖的计时器
var isAutoSkipping = false; // 防止重复触发翻页
var lastGameIds = ''; // 记录上一次的游戏ID列表,用于检测数据是否更新
var autoSkipTargetPage = 0; // 自动跳页的目标页码
var isFastScanning = false; // 快速扫描进行中
var fastScanAbort = false; // 中止快速扫描
var blacklist = []; // 黑名单 [{appId, name, addedTime}]
var noBlacklistGames = false; // 不显示黑名单游戏
var noBundledGames = false; // 不显示进过慈善包的游戏
var scanHideAllGames = false; // 扫描模式:隐藏全部当前结果,用于收集待确认 appid
var bundledAppsData = null; // barter.vg 慈善包数据 {appid: bundleCount}
var profileConfirmDB = {}; // 本地状态库 {appid: {status, appType, checkedAt}}
var profileConfirmQueue = []; // 等待写入本地状态库的 appid
var profileConfirmSaveTimer = null;
var profileConfirmScanTimer = null;
var cdkeyCheckQueue = new Set();
var cdkeyCheckTimer = null;
var profileConfirmScanning = false;
var profileConfirmAbort = false;
var profileConfirmAutoScan = false;
var settingsVm = null;

const PROFILE_CONFIRM_DB_KEY = 'ProfileConfirmDB';
const PROFILE_CONFIRM_QUEUE_KEY = 'ProfileConfirmQueue';
const PROFILE_CONFIRM_AUTO_SCAN_KEY = 'ProfileConfirmAutoScan';
const PROFILE_CONFIRM_AUTO_SCAN_MIGRATED_KEY = 'ProfileConfirmAutoScanDefaultOff';
const PROFILE_CONFIRM_CONCURRENT = 6;
const PROFILE_CONFIRM_BATCH_DELAY = 200;
const PROFILE_CONFIRM_TIMEOUT = 8000;
const PROFILE_CONFIRM_EXPORT_VERSION = 1;


(function () {
  'use strict';
  load();
  observePageChanges();
})();

// 判断单个 appId 是否符合当前筛选条件(纯数据判断,不依赖 DOM)
function doesAppPassFilter(appId) {
  appId = Number(appId);
  if (!appId) return false;

  // 下架游戏筛选
  if (onlyDelistedGames) {
    if (!delistedGamesData || !delistedGamesData.removed_apps) return false;
    const gameData = delistedGamesData.removed_apps[appId.toString()];
    if (!gameData) return false;
    const categoryMap = {
      'Purchase disabled': 'purchase disabled',
      'Delisted': 'delisted',
      'Delisted video': 'delisted',
      'F2P': 'f2p',
      'Unreleased': 'unreleased',
      'Test app': 'test app',
      'Retail only': 'retail only',
      'Pre-order exclusive': 'pre-order exclusive',
      'Banned': 'banned'
    };
    const mappedType = categoryMap[gameData.category] || (gameData.category || '').toLowerCase();
    if (!delistedTypes.includes(mappedType)) return false;
  }

  // 不显示 DLC
  if (noDlc && isNoGameApp(appId)) return false;

  // 不显示已拥有
  if (noownedGames && Saves.ownedApps.includes(appId)) return false;

  rememberProfileConfirmCandidate(appId);

  if (scanHideAllGames) return false;

  // Hide only apps confirmed by the local profile-status DB.
  if (noRestrictedGames && localStorage.getItem('IsProfileFeatureLimited') && isProfileRestrictedByConfirmedStatus(appId)) return false;

  // 不显示黑名单
  if (noBlacklistGames && blacklist.some(b => b.appId === appId)) return false;

  // 不显示进过慈善包的游戏
  if (noBundledGames && bundledAppsData) {
    const bundleCount = bundledAppsData[appId.toString()];
    if (bundleCount && bundleCount > 0) return false;
  }

  return true;
}

// 黑名单操作
function addToBlacklist(appId, name) {
  appId = Number(appId);
  if (blacklist.some(b => b.appId === appId)) return;
  blacklist.push({ appId, name: name || ('App ' + appId), addedTime: Date.now() });
  GM_setValue('Blacklist', blacklist);
}

function removeFromBlacklist(appId) {
  appId = Number(appId);
  blacklist = blacklist.filter(b => b.appId !== appId);
  GM_setValue('Blacklist', blacklist);
}

function isInBlacklist(appId) {
  return blacklist.some(b => b.appId === Number(appId));
}

function setLimitedApps(list) {
  limitedApps = Array.isArray(list) ? list.map(Number).filter(id => id > 0) : [];
  limitedAppsSet = new Set(limitedApps);
}

function rebuildLocalLimitedApps() {
  const localNormalApps = Object.keys(profileConfirmDB || {})
    .filter((appid) => {
      const entry = profileConfirmDB[appid];
      return entry && entry.status === 'normal' && entry.appType !== 'dlc' && entry.appType !== 'non_game';
    })
    .map(Number)
    .filter(id => id > 0);
  setLimitedApps(localNormalApps);
  GM_setValue('limitedApps', limitedApps);
  return limitedApps;
}

function isBaseNormalApp(appId) {
  return limitedAppsSet.has(Number(appId));
}

function isOwnedApp(appId) {
  return Saves.ownedApps.includes(Number(appId));
}

function setNoGameList(list) {
  noGameList = Array.isArray(list) ? list.map(Number).filter(id => id > 0) : [];
  noGameListSet = new Set(noGameList);
}

function isNoGameApp(appId) {
  const appType = getLocalAppType(appId);
  return appType === 'dlc' || appType === 'non_game';
}

function isUnavailableProfileStatus(status) {
  return status === 'restricted' || status === 'learning' || status === 'banned';
}

function getProfileConfirmEntry(appId) {
  return profileConfirmDB[String(Number(appId))] || null;
}

function getProfileConfirmStatus(appId) {
  const entry = getProfileConfirmEntry(appId);
  return entry && entry.status ? entry.status : null;
}

function getLocalAppType(appId) {
  const entry = getProfileConfirmEntry(appId);
  return entry && entry.appType ? entry.appType : null;
}

function isLocalStatusComplete(appId) {
  const entry = getProfileConfirmEntry(appId);
  return !!(entry && entry.status && entry.appType);
}

function rebuildLocalNoGameList() {
  const localNoGameApps = Object.keys(profileConfirmDB || {})
    .filter((appid) => {
      const appType = profileConfirmDB[appid] && profileConfirmDB[appid].appType;
      return appType === 'dlc' || appType === 'non_game';
    })
    .map(Number)
    .filter(id => id > 0);
  setNoGameList(localNoGameApps);
  GM_setValue('NoGameList', noGameList);
  return noGameList;
}

function isValidProfileConfirmStatus(status) {
  return status === 'normal' || status === 'restricted' || status === 'learning' || status === 'banned';
}

function isValidLocalAppType(appType) {
  return appType === 'game' || appType === 'dlc' || appType === 'non_game';
}

function isProfileRestrictedByConfirmedStatus(appId) {
  if (isBannedApp(appId)) return true;
  const status = getProfileConfirmStatus(appId);
  if (status) return isUnavailableProfileStatus(status);
  return false;
}

function getDelistedCategory(appId) {
  if (!delistedGamesData || !delistedGamesData.removed_apps) return null;
  const gameData = delistedGamesData.removed_apps[String(Number(appId))];
  if (!gameData || !gameData.category) return null;
  const categoryMap = {
    'Purchase disabled': 'purchase disabled',
    'Delisted': 'delisted',
    'Delisted video': 'delisted',
    'F2P': 'f2p',
    'Unreleased': 'unreleased',
    'Test app': 'test app',
    'Retail only': 'retail only',
    'Pre-order exclusive': 'pre-order exclusive',
    'Banned': 'banned'
  };
  return categoryMap[gameData.category] || String(gameData.category).toLowerCase();
}

function isBannedApp(appId) {
  return getDelistedCategory(appId) === 'banned';
}

function saveProfileConfirmDataSoon() {
  if (profileConfirmSaveTimer) return;
  profileConfirmSaveTimer = setTimeout(() => {
    profileConfirmSaveTimer = null;
    normalizeProfileConfirmQueue();
    rebuildLocalLimitedApps();
    rebuildLocalNoGameList();
    GM_setValue(PROFILE_CONFIRM_DB_KEY, profileConfirmDB);
    GM_setValue(PROFILE_CONFIRM_QUEUE_KEY, profileConfirmQueue);
    updateProfileConfirmStats();
  }, 500);
}

function saveProfileConfirmDataNow() {
  if (profileConfirmSaveTimer) {
    clearTimeout(profileConfirmSaveTimer);
    profileConfirmSaveTimer = null;
  }
  normalizeProfileConfirmQueue();
  rebuildLocalLimitedApps();
  rebuildLocalNoGameList();
  GM_setValue(PROFILE_CONFIRM_DB_KEY, profileConfirmDB);
  GM_setValue(PROFILE_CONFIRM_QUEUE_KEY, profileConfirmQueue);
  updateProfileConfirmStats();
}

function getProfileConfirmStats() {
  const stats = {
    total: 0,
    normal: 0,
    restricted: 0,
    learning: 0,
    banned: 0,
    dlc: 0,
    nonGame: 0,
    game: 0,
    unknown: 0,
    queued: profileConfirmQueue.length
  };
  Object.values(profileConfirmDB || {}).forEach((entry) => {
    stats.total++;
    const status = entry && entry.status;
    const appType = entry && entry.appType;
    if (status === 'banned') {
      stats.banned++;
      stats.restricted++;
    }
    else if (stats[status] !== undefined) stats[status]++;
    if (appType === 'dlc') stats.dlc++;
    else if (appType === 'non_game') stats.nonGame++;
    else if (appType === 'game') stats.game++;
    else stats.unknown++;
  });
  return stats;
}

function normalizeProfileConfirmQueue() {
  const seen = new Set();
  profileConfirmQueue = (profileConfirmQueue || []).map(Number).filter((appId) => {
    if (!appId || seen.has(appId) || isOwnedApp(appId) || isNoGameApp(appId) || isLocalStatusComplete(appId)) return false;
    seen.add(appId);
    return true;
  });
}

function updateProfileConfirmStats() {
  if (!settingsVm) return;
  const stats = getProfileConfirmStats();
  settingsVm.profileConfirmTotal = stats.total;
  settingsVm.profileConfirmAddable = limitedApps.length;
  settingsVm.profileConfirmNormal = stats.normal;
  settingsVm.profileConfirmRestricted = stats.restricted;
  settingsVm.profileConfirmLearning = stats.learning;
  settingsVm.profileConfirmBanned = stats.banned;
  settingsVm.profileConfirmDlc = stats.dlc;
  settingsVm.profileConfirmNonGame = stats.nonGame;
  settingsVm.profileConfirmUnknown = stats.unknown;
  settingsVm.profileConfirmQueued = stats.queued;
  settingsVm.profileConfirmScanning = profileConfirmScanning;
  settingsVm.profileConfirmAutoScan = profileConfirmAutoScan;
}

function ensureProfileConfirmAutoScan(delay = 1000) {
  normalizeProfileConfirmQueue();
  updateProfileConfirmStats();
  if (!profileConfirmAutoScan || profileConfirmQueue.length === 0 || profileConfirmScanning) return;
  scheduleProfileConfirmScan(delay);
}

function rememberProfileConfirmCandidate(appId, force = false) {
  appId = Number(appId);
  if (isOwnedApp(appId)) return;
  if (isNoGameApp(appId)) return;
  if (!appId) return;
  if (isLocalStatusComplete(appId)) return;
  if (profileConfirmQueue.includes(appId)) return;
  profileConfirmQueue.push(appId);
  saveProfileConfirmDataSoon();
  if (force || profileConfirmAutoScan) scheduleProfileConfirmScan(200, force);
}

function rememberProfileConfirmCandidates(appIds, force = false) {
  let changed = false;
  (appIds || []).forEach((appId) => {
    appId = Number(appId);
    if (isOwnedApp(appId)) return;
    if (isNoGameApp(appId)) return;
    if (!appId) return;
    if (isLocalStatusComplete(appId)) return;
    if (profileConfirmQueue.includes(appId)) return;
    profileConfirmQueue.push(appId);
    changed = true;
  });
  if (changed) {
    saveProfileConfirmDataSoon();
    if (force || profileConfirmAutoScan) scheduleProfileConfirmScan(200, force);
  }
}

function legacyProfileStatusOnlyCheck(appId) {
  if (isBannedApp(appId)) return Promise.resolve('banned');
  return new Promise((resolve) => {
    GM_xmlhttpRequest({
      method: 'GET',
      url: `https://store.steampowered.com/app/${appId}?cc=cn&l=schinese`,
      headers: { Cookie: 'wants_mature_content=1;birthtime=0;lastagecheckage=1-0-1990;' },
      timeout: PROFILE_CONFIRM_TIMEOUT,
      onload(resp) {
        const html = resp.responseText || '';
        if (html.includes('个人资料功能受限') || html.includes('Profile Features Limited')) {
          resolve('restricted');
        } else if (html.includes('Steam 正在了解') || html.includes('Steam Is Learning About')) {
          resolve('learning');
        } else {
          resolve('normal');
        }
      },
      onerror() { resolve(null); },
      ontimeout() { resolve(null); },
    });
  });
}

function getStoreHtmlProfileStatus(html) {
  if (html.includes('个人资料功能受限') || html.includes('Profile Features Limited')) return 'restricted';
  if (html.includes('Steam 正在了解') || html.includes('Steam Is Learning About')) return 'learning';
  return 'normal';
}

function getStoreHtmlAppType(html) {
  if (html.includes('game_area_dlc_bubble') || html.includes('This content requires') || html.includes('Downloadable Content')) return 'dlc';
  if (html.includes('category1=990') || html.includes('category1=994') || html.includes('All Soundtracks') || html.includes('All Software')) return 'non_game';
  return 'game';
}

function getStoreAppTypeByApi(appId) {
  return new Promise((resolve) => {
    GM_xmlhttpRequest({
      method: 'GET',
      url: `https://store.steampowered.com/api/appdetails?appids=${appId}&filters=basic&cc=cn&l=english`,
      responseType: 'json',
      timeout: PROFILE_CONFIRM_TIMEOUT,
      onload(resp) {
        try {
          const data = typeof resp.response === 'object' && resp.response
            ? resp.response
            : JSON.parse(resp.responseText || '{}');
          const appData = data[String(appId)] && data[String(appId)].data;
          const type = appData && appData.type;
          if (type === 'dlc') resolve('dlc');
          else if (type === 'game') resolve('game');
          else resolve('non_game');
        } catch (e) {
          resolve(null);
        }
      },
      onerror() { resolve(null); },
      ontimeout() { resolve(null); },
    });
  });
}

async function checkLocalAppStatus(appId) {
  if (isBannedApp(appId)) return { status: 'banned', appType: getLocalAppType(appId) || 'game' };

  const appTypePromise = getStoreAppTypeByApi(appId);
  return new Promise((resolve) => {
    GM_xmlhttpRequest({
      method: 'GET',
      url: `https://store.steampowered.com/app/${appId}?cc=cn&l=schinese`,
      headers: { Cookie: 'wants_mature_content=1;birthtime=0;lastagecheckage=1-0-1990;' },
      timeout: PROFILE_CONFIRM_TIMEOUT,
      async onload(resp) {
        const html = resp.responseText || '';
        const appType = await appTypePromise;
        resolve({
          status: getStoreHtmlProfileStatus(html),
          appType: appType || getStoreHtmlAppType(html)
        });
      },
      onerror() { resolve(null); },
      ontimeout() { resolve(null); },
    });
  });
}

function setProfileConfirmResult(appId, result) {
  appId = Number(appId);
  if (!appId || !result) return;
  const status = typeof result === 'string' ? result : result.status;
  if (!isValidProfileConfirmStatus(status)) return;
  const old = profileConfirmDB[String(appId)] || {};
  const appType = typeof result === 'object' && isValidLocalAppType(result.appType)
    ? result.appType
    : (isValidLocalAppType(old.appType) ? old.appType : 'game');
  profileConfirmDB[String(appId)] = {
    ...old,
    status,
    appType,
    checkedAt: Date.now()
  };
}

function upsertProfileConfirmResult(appId, result) {
  appId = Number(appId);
  if (!appId || !result) return;
  setProfileConfirmResult(appId, result);
  profileConfirmQueue = profileConfirmQueue.filter(id => Number(id) !== appId);
  saveProfileConfirmDataSoon();
}

function scheduleProfileConfirmScan(delay = 2000, force = false) {
  if ((!force && !profileConfirmAutoScan) || profileConfirmQueue.length === 0 || profileConfirmScanning || profileConfirmScanTimer) return;
  profileConfirmScanTimer = setTimeout(() => {
    profileConfirmScanTimer = null;
    runProfileConfirmScan();
  }, delay);
}

async function runProfileConfirmScan(limit = 3000) {
  if (profileConfirmScanning) return;
  profileConfirmScanning = true;
  profileConfirmAbort = false;
  updateProfileConfirmStats();

  let checked = 0;
  let attempted = 0;
  while (!profileConfirmAbort && profileConfirmQueue.length > 0 && attempted < limit) {
    const batch = [];
    while (!profileConfirmAbort && profileConfirmQueue.length > 0 && batch.length < PROFILE_CONFIRM_CONCURRENT && attempted + batch.length < limit) {
      const appId = Number(profileConfirmQueue.shift());
      if (!appId || isOwnedApp(appId) || isNoGameApp(appId) || isLocalStatusComplete(appId)) {
        continue;
      }
      batch.push(appId);
    }

    if (batch.length === 0) {
      break;
    }
    attempted += batch.length;

    const results = await Promise.all(batch.map(async (appId) => ({
      appId,
      result: await checkLocalAppStatus(appId)
    })));

    let failedCount = 0;
    let hasSavedResult = false;
    results.forEach(({ appId, result }) => {
      if (result && result.status) {
        setProfileConfirmResult(appId, result);
        checked++;
        hasSavedResult = true;
      } else {
        failedCount++;
        if (!profileConfirmQueue.includes(appId) && !isLocalStatusComplete(appId)) {
          profileConfirmQueue.push(appId);
        }
      }
    });
    if (hasSavedResult) {
      saveProfileConfirmDataSoon();
    }

    updateProfileConfirmStats();
    if (failedCount === batch.length) {
      break;
    }
    if (PROFILE_CONFIRM_BATCH_DELAY > 0 && profileConfirmQueue.length > 0 && attempted < limit) {
      await new Promise(resolve => setTimeout(resolve, PROFILE_CONFIRM_BATCH_DELAY));
    }
  }

  const wasAborted = profileConfirmAbort;
  profileConfirmScanning = false;
  profileConfirmAbort = false;
  saveProfileConfirmDataNow();

  queueAllCdkeyGameCheckers();
  if (noRestrictedGames || scanHideAllGames) checkAndAutoSkip();

  if (!wasAborted && profileConfirmAutoScan && profileConfirmQueue.length > 0) {
    scheduleProfileConfirmScan(300);
  }
}

function abortProfileConfirmScan() {
  profileConfirmAbort = true;
  if (profileConfirmScanTimer) {
    clearTimeout(profileConfirmScanTimer);
    profileConfirmScanTimer = null;
  }
  updateProfileConfirmStats();
}

function clearProfileConfirmData() {
  profileConfirmDB = {};
  profileConfirmQueue = [];
  saveProfileConfirmDataNow();
}

function queueCdkeyGameChecker(element) {
  if (!element || !element.classList || !element.classList.contains('cdkGameIcon')) return;
  cdkeyCheckQueue.add(element);
  if (cdkeyCheckTimer) return;
  const run = window.requestIdleCallback || window.requestAnimationFrame || ((fn) => setTimeout(fn, 16));
  cdkeyCheckTimer = run(processCdkeyCheckQueue);
}

function queueAllCdkeyGameCheckers() {
  document.querySelectorAll('.cdkGameIcon').forEach((element) => {
    queueCdkeyGameChecker(element);
  });
}

function processCdkeyCheckQueue() {
  cdkeyCheckTimer = null;
  const items = Array.from(cdkeyCheckQueue).filter((element) => element && element.isConnected);
  cdkeyCheckQueue.clear();
  const chunk = items.splice(0, 12);
  chunk.forEach((element) => {
    try {
      cdkeyGameChecker(element);
    } catch (e) {
      console.error('[Better Steampy] 标记游戏卡片失败:', e);
    }
  });
  items.forEach((element) => cdkeyCheckQueue.add(element));
  if (cdkeyCheckQueue.size > 0) {
    const run = window.requestIdleCallback || window.requestAnimationFrame || ((fn) => setTimeout(fn, 16));
    cdkeyCheckTimer = run(processCdkeyCheckQueue);
  }
}

function fixBannedProfileConfirmResults() {
  if (!delistedGamesData || !delistedGamesData.removed_apps) return 0;
  let fixed = 0;
  Object.keys(profileConfirmDB || {}).forEach((appid) => {
    if (isBannedApp(appid) && profileConfirmDB[appid].status !== 'banned') {
      profileConfirmDB[appid] = {
        status: 'banned',
        checkedAt: Date.now()
      };
      fixed++;
    }
  });
  if (fixed > 0) {
    saveProfileConfirmDataNow();
  }
  return fixed;
}

function exportProfileConfirmData() {
  const stats = getProfileConfirmStats();
  const payload = {
    name: 'Better SteamPYPlus local status library',
    version: PROFILE_CONFIRM_EXPORT_VERSION,
    exportedAt: Date.now(),
    stats: {
      normal: stats.normal,
      restricted: stats.restricted,
      learning: stats.learning,
      total: stats.normal + stats.restricted + stats.learning
    },
    db: profileConfirmDB
  };

  const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' });
  const url = URL.createObjectURL(blob);
  const date = new Date().toISOString().slice(0, 10);
  const a = document.createElement('a');
  a.href = url;
  a.download = `steampy_local_status_${date}_${payload.stats.total}.json`;
  document.body.appendChild(a);
  a.click();
  a.remove();
  URL.revokeObjectURL(url);
}

function getProfileConfirmImportDB(data) {
  if (!data || typeof data !== 'object') return null;
  if (data.db && typeof data.db === 'object') return data.db;
  if (data.ProfileConfirmDB && typeof data.ProfileConfirmDB === 'object') return data.ProfileConfirmDB;
  return data;
}

function importProfileConfirmData(data) {
  const importDB = getProfileConfirmImportDB(data);
  if (!importDB || typeof importDB !== 'object') {
    throw new Error('invalid import data');
  }

  let imported = 0;
  let skipped = 0;
  let updated = 0;
  Object.entries(importDB).forEach(([appid, value]) => {
    const id = Number(appid);
    if (!id || isOwnedApp(id)) {
      skipped++;
      return;
    }

    const status = typeof value === 'string' ? value : value && value.status;
    if (!isValidProfileConfirmStatus(status)) {
      skipped++;
      return;
    }
    const appType = value && isValidLocalAppType(value.appType) ? value.appType : 'game';

    const checkedAt = Number((value && value.checkedAt) || Date.now());
    const key = String(id);
    const old = profileConfirmDB[key];
    if (old && old.checkedAt && Number(old.checkedAt) > checkedAt) {
      skipped++;
      return;
    }

    profileConfirmDB[key] = { status, appType, checkedAt };
    if (old) updated++;
    else imported++;
  });

  normalizeProfileConfirmQueue();
  fixBannedProfileConfirmResults();
  saveProfileConfirmDataNow();
  return { imported, updated, skipped };
}

function importProfileConfirmDataFromFile() {
  const input = document.createElement('input');
  input.type = 'file';
  input.accept = 'application/json,.json';
  input.onchange = () => {
    const file = input.files && input.files[0];
    if (!file) return;
    const reader = new FileReader();
    reader.onload = () => {
      try {
        const data = JSON.parse(reader.result);
        const result = importProfileConfirmData(data);
        iview.Notice.success({
          title: 'Better Steampy',
          desc: `导入完成:新增 ${result.imported},更新 ${result.updated},跳过 ${result.skipped}`,
          duration: 5
        });
        queueAllCdkeyGameCheckers();
        if (noRestrictedGames || scanHideAllGames) checkAndAutoSkip();
      } catch (e) {
        console.error('[Better Steampy] 导入本地状态库失败:', e);
        iview.Notice.error({
          title: 'Better Steampy',
          desc: '导入失败,请确认文件是有效的本地状态库 JSON',
          duration: 5
        });
      }
    };
    reader.readAsText(file);
  };
  input.click();
}

// 获取 Vue 分页父组件及翻页方法
function getPageComponent() {
  try {
    const pageComp = document.querySelector('.ivu-page').__vue__;
    for (let i = 1; i <= 6; i++) {
      let comp = pageComp;
      for (let j = 0; j < i; j++) {
        comp = comp.$parent;
        if (!comp) break;
      }
      if (comp && (comp.pageHandler || comp.changePage)) {
        return comp;
      }
    }
  } catch (e) {}
  return null;
}

// 获取当前页码和总页数
function getPageInfo() {
  const activePageEl = document.querySelector('.zpagenav .page-ul li.active');
  let currentPage = 1, maxPage = 1;
  if (activePageEl) {
    currentPage = parseInt(activePageEl.textContent) || 1;
    const allPageNums = Array.from(document.querySelectorAll('.zpagenav .page-ul li'))
      .map(li => parseInt(li.textContent))
      .filter(num => !isNaN(num));
    maxPage = Math.max(...allPageNums, 1);
  }
  return { currentPage, maxPage };
}

// 跳转到指定页码
function jumpToPage(pageNum) {
  const comp = getPageComponent();
  if (comp) {
    if (comp.pageHandler) {
      comp.pageHandler(pageNum);
    } else if (comp.changePage) {
      comp.changePage(pageNum);
    }
    return true;
  }
  // fallback: 点击按钮
  const nextButton = document.querySelector('.ivu-page-next:not(.ivu-page-disabled)');
  if (nextButton) { nextButton.click(); return true; }
  return false;
}

// 从当前 DOM 中提取所有游戏的 appId
function extractCurrentPageAppIds() {
  const icons = document.querySelectorAll('.cdkGameIcon');
  return Array.from(icons).map(icon => {
    const src = icon.getAttribute('data-src') || '';
    const match = src.match(/\/apps\/(\d+)\//);
    return match ? Number(match[1]) : 0;
  }).filter(id => id > 0);
}

// 生成当前页数据的指纹
function getPageDataFingerprint() {
  const icons = document.querySelectorAll('.cdkGameIcon');
  return Array.from(icons).map(icon => icon.getAttribute('data-src') || '').join('|');
}

// 事件驱动等待:用 MutationObserver 监听 data-src 变化,比轮询快得多
function waitForPageData(expectedPage, prevFingerprint, timeout = 8000) {
  return new Promise((resolve) => {
    let settled = false;
    let timeoutId = null;
    let observer = null;

    const cleanup = () => {
      if (settled) return;
      settled = true;
      if (observer) observer.disconnect();
      if (timeoutId) clearTimeout(timeoutId);
    };

    const tryResolve = () => {
      if (settled) return;
      if (fastScanAbort) { cleanup(); resolve(false); return; }
      const { currentPage } = getPageInfo();
      const fp = getPageDataFingerprint();
      const icons = document.querySelectorAll('.cdkGameIcon');
      if (currentPage === expectedPage && fp !== prevFingerprint && fp.length > 0 && icons.length > 0) {
        cleanup();
        resolve(true);
      }
    };

    // 超时保底
    timeoutId = setTimeout(() => {
      if (!settled) {
        console.log('[Better Steampy] 第', expectedPage, '页等待超时');
        cleanup();
        resolve(false);
      }
    }, timeout);

    // 先立即检查(可能数据已经到了)
    tryResolve();
    if (settled) return;

    // MutationObserver 监听 data-src 变化——Vue 更新 DOM 时立即触发
    observer = new MutationObserver(() => { tryResolve(); });
    const container = document.querySelector('.cdkGameIcon')?.closest('.gameblock')?.parentElement?.parentElement || document.body;
    observer.observe(container, { attributes: true, attributeFilter: ['data-src'], subtree: true, childList: true });
  });
}

// 创建/获取扫描进度浮窗(单例,固定位置,只更新文字)
function getScanOverlay() {
  let el = document.getElementById('bsp-scan-overlay');
  if (!el) {
    el = document.createElement('div');
    el.id = 'bsp-scan-overlay';
    el.style.cssText = 'position:fixed;top:16px;right:16px;z-index:99999;background:rgba(0,0,0,0.78);color:#fff;padding:12px 22px;border-radius:8px;font-size:14px;pointer-events:auto;display:none;box-shadow:0 2px 12px rgba(0,0,0,0.3);max-width:320px;line-height:1.6;';
    // 停止按钮
    const btn = document.createElement('span');
    btn.id = 'bsp-scan-stop';
    btn.textContent = '  ✕ 停止';
    btn.style.cssText = 'cursor:pointer;margin-left:10px;color:#ff6b6b;font-weight:bold;';
    btn.onclick = () => { fastScanAbort = true; };
    el.appendChild(document.createElement('span')); // 文字容器
    el.appendChild(btn);
    document.body.appendChild(el);
  }
  return el;
}

function showScanProgress(text) {
  const el = getScanOverlay();
  el.style.display = 'block';
  el.querySelector('span:first-child').textContent = text;
}

function hideScanOverlay() {
  const el = document.getElementById('bsp-scan-overlay');
  if (el) el.style.display = 'none';
}

// 尝试从 Vue 组件直接读取游戏列表数据(比 DOM 快,不需要等渲染)
function tryGetVueGameList() {
  try {
    const comp = getPageComponent();
    if (!comp) return null;
    // 遍历组件及其子组件,查找包含游戏列表的 data
    const searchData = (vm) => {
      if (!vm) return null;
      const d = vm.$data || vm._data || {};
      for (const key of Object.keys(d)) {
        const val = d[key];
        if (Array.isArray(val) && val.length > 0 && val[0] && (val[0].appid || val[0].appId || val[0].app_id || val[0].icon)) {
          return val;
        }
      }
      // 搜索子组件
      if (vm.$children) {
        for (const child of vm.$children) {
          const result = searchData(child);
          if (result) return result;
        }
      }
      return null;
    };
    // 从分页组件向上找到根列表组件
    let target = comp;
    for (let i = 0; i < 3; i++) {
      const result = searchData(target);
      if (result) return result;
      if (target.$parent) target = target.$parent;
      else break;
    }
  } catch (e) {}
  return null;
}

// 从 Vue 数据中提取 appId
function extractAppIdsFromVueData(list) {
  return list.map(item => {
    // 尝试各种可能的字段名
    const id = item.appid || item.appId || item.app_id || 0;
    if (id) return Number(id);
    // 从 icon URL 提取
    const icon = item.icon || item.img || '';
    const match = icon.match(/\/apps\/(\d+)\//);
    return match ? Number(match[1]) : 0;
  }).filter(id => id > 0);
}

// 快速扫描:跳过不符合条件的页面,直接定位到有结果的页面
async function fastScanPages(startPage, maxPage) {
  if (isFastScanning) return;
  isFastScanning = true;
  fastScanAbort = false;

  showScanProgress(`快速扫描中... ${startPage} / ${maxPage}`);

  let foundPage = -1;

  for (let page = startPage; page <= maxPage; page++) {
    if (fastScanAbort) {
      console.log('[Better Steampy] 快速扫描被中止');
      break;
    }

    showScanProgress(`快速扫描中... ${page} / ${maxPage}`);

    // 记录跳转前的数据指纹
    const prevFingerprint = getPageDataFingerprint();

    jumpToPage(page);

    // 等待数据更新
    const loaded = await waitForPageData(page, prevFingerprint, 8000);
    if (!loaded) {
      console.log('[Better Steampy] 第', page, '页加载超时,跳过');
      continue;
    }

    // 优先从 Vue 数据读取(更快更可靠),fallback 到 DOM
    let appIds;
    const vueList = tryGetVueGameList();
    if (vueList) {
      appIds = extractAppIdsFromVueData(vueList);
    }
    if (!appIds || appIds.length === 0) {
      appIds = extractCurrentPageAppIds();
    }

    rememberProfileConfirmCandidates(appIds);

    const hasMatch = appIds.some(id => doesAppPassFilter(id));

    if (hasMatch) {
      foundPage = page;
      console.log('[Better Steampy] 在第', page, '页找到符合条件的游戏');
      break;
    } else {
      console.log('[Better Steampy] 第', page, '页无符合条件的游戏,继续扫描');
    }
  }

  hideScanOverlay();

  if (foundPage > 0) {
    queueAllCdkeyGameCheckers();

    iview.Notice.success({
      title: 'Better Steampy',
      desc: `已跳转到第 ${foundPage} 页(跳过了 ${foundPage - startPage} 页无结果页面)`,
      duration: 3
    });
  } else if (!fastScanAbort) {
    iview.Notice.warning({
      title: 'Better Steampy',
      desc: `第 ${startPage} ~ ${maxPage} 页均无符合条件的游戏`,
      duration: 4
    });
    if (startPage > 1) {
      jumpToPage(startPage);
    }
  }

  isFastScanning = false;
  isAutoSkipping = false;
  autoSkipTargetPage = 0;
}

// 检测当前页是否为空,如果为空则启动快速扫描
function checkAndAutoSkip() {
  if (autoSkipTimer) clearTimeout(autoSkipTimer);
  if (isFastScanning) return;

  autoSkipTimer = setTimeout(() => {
    const cdkGameIcons = document.querySelectorAll('.cdkGameIcon');
    const allGames = Array.from(cdkGameIcons).map(icon => icon.closest('.gameblock')).filter(el => el !== null);
    const visibleGames = allGames.filter(el => el.style.display !== 'none');

    // 获取当前页面游戏的ID列表
    const currentGameIds = Array.from(cdkGameIcons).map(icon => {
      const src = icon.getAttribute('data-src') || '';
      const match = src.match(/\/apps\/(\d+)\//);
      return match ? match[1] : '';
    }).join(',');

    const dataChanged = currentGameIds !== lastGameIds && lastGameIds !== '';

    if (isAutoSkipping) {
      if (!dataChanged) return;
      isAutoSkipping = false;
    } else if (dataChanged) {
      autoSkipTargetPage = 0;
    }

    lastGameIds = currentGameIds;

    // 当前页有游戏但全部被隐藏
    if (allGames.length > 0 && visibleGames.length === 0) {
      const { currentPage, maxPage } = getPageInfo();

      if (currentPage >= maxPage) {
        iview.Notice.warning({
          title: 'Better Steampy',
          desc: '当前页无符合条件的游戏,已到最后一页',
          duration: 3
        });
        return;
      }

      // 启动快速扫描,从下一页开始
      fastScanPages(currentPage + 1, maxPage);
    } else {
      isAutoSkipping = false;
      autoSkipTargetPage = 0;
    }
  }, 500);
}

// 重置自动跳页状态(用于手动翻页时调用)
function resetAutoSkipState() {
  isAutoSkipping = false;
  autoSkipTargetPage = 0;
  // 如果正在快速扫描,中止它
  if (isFastScanning) {
    fastScanAbort = true;
  }
}

//读取个人库存及愿望单并储存
function getOwnAndWish() {
  return new Promise((resolve, reject) => {
    var wishlist = [];
    var ownedApps = [];
    GM_xmlhttpRequest({
      method: 'GET',
      url:
        'https://store.steampowered.com/dynamicstore/userdata/?t=' +
        Math.trunc(Date.now() / 1000),
      responseType: 'json',
      onload: function (response) {
        let data = JSON.parse(response.responseText);
        wishlist = data.rgWishlist;
        ownedApps = data.rgOwnedApps;
        let previousSaves = GM_getValue('Saves');
        let newSave = {
          wishlist: wishlist,
          ownedApps: ownedApps,
          familygameList: previousSaves.familygameList,
          lastupdatetime: new Date().getTime(),
        };
        GM_setValue('Saves', newSave);
        Saves = newSave;
        saveProfileConfirmDataNow();
        iview.Notice.success({
          title: `Better Steampy`,
          desc: `已加载 ${ownedApps.length} 个库存游戏及DLC,${wishlist.length} 个愿望单游戏`,
        });
        resolve(newSave);
      },
    });
  });
}

//读取家庭库并储存
function getFamilyGame() {
  return new Promise((resolve, reject) => {
    var access_token;
    var family_groupid;
    var familygameList = [];
    GM_xmlhttpRequest({
      method: 'GET',
      url: 'https://store.steampowered.com/pointssummary/ajaxgetasyncconfig',
      responseType: 'json',
      onload: function (response) {
        let data = JSON.parse(response.responseText);
        access_token = data.data.webapi_token; // access_token
        GM_xmlhttpRequest({
          method: 'GET',
          url: `https://api.steampowered.com/IFamilyGroupsService/GetFamilyGroupForUser/v1/?access_token=${access_token}`,
          responseType: 'json',
          onload: function (response) {
            let data = JSON.parse(response.responseText);
            family_groupid = data.response.family_groupid; // family_groupid
            GM_xmlhttpRequest({
              method: 'GET',
              url: `https://api.steampowered.com/IFamilyGroupsService/GetSharedLibraryApps/v1/?access_token=${access_token}&family_groupid=${family_groupid}&include_own=true`,
              responseType: 'json',
              onload: function (response) {
                let data = JSON.parse(response.responseText);
                data.response.apps.forEach((app) => {
                  if (app.exclude_reason == 0) {
                    familygameList.push(app.appid);
                  }
                });
                let previousSaves = GM_getValue('Saves');
                let newSave = {
                  wishlist: previousSaves.wishlist,
                  ownedApps: previousSaves.ownedApps,
                  familygameList: familygameList,
                  lastupdatetime: new Date().getTime(),
                };
                GM_setValue('Saves', newSave);
                Saves = newSave;
                iview.Notice.success({
                  title: `Better Steampy`,
                  desc: `已加载 ${familygameList.length} 个家庭库游戏`,
                });
                resolve(familygameList);
              },
            });
          },
        });
      },
    });
  });
}

// Rebuild the local +1 library from the user's own confirmed profile-status DB.
function getLimitedGamesList() {
  return new Promise((resolve) => {
    const appIds = typeof extractCurrentPageAppIds === 'function' ? extractCurrentPageAppIds() : [];
    rememberProfileConfirmCandidates(appIds);
    const localNormalApps = rebuildLocalLimitedApps();
    iview.Notice.success({
      title: `Better Steampy`,
      desc: `已收集当前页 App,待扫描 ${profileConfirmQueue.length} 个;本地可加一 ${localNormalApps.length} 个`,
    });
    resolve(localNormalApps);
  });
}

//获取非游戏列表
function getNogameList() {
  return new Promise((resolve) => {
    const localNoGameApps = rebuildLocalNoGameList();
    iview.Notice.success({
      title: `Better Steampy`,
      desc: `已从本地状态库加载 ${localNoGameApps.length} 个DLC及非游戏 App`,
    });
    resolve(localNoGameApps);
  });
}

//获取下架游戏列表(尝试从 SWI 插件缓存读取)
function getDelistedGamesList() {
  return new Promise((resolve, reject) => {
    console.log('[Better Steampy] 开始获取下架游戏数据...');

    // 尝试从 chrome.storage 读取 SWI 插件的缓存数据
    if (typeof chrome !== 'undefined' && chrome.storage && chrome.storage.local) {
      chrome.storage.local.get(['swi_decommissioned'], function(result) {
        if (result.swi_decommissioned && result.swi_decommissioned.removed_apps) {
          console.log('[Better Steampy] 从 SWI 插件缓存读取下架游戏数据');
          delistedGamesData = result.swi_decommissioned;
          GM_setValue('DelistedGamesData', delistedGamesData);
          fixBannedProfileConfirmResults();
          const count = Object.keys(delistedGamesData.removed_apps).length;
          console.log('[Better Steampy] 成功加载下架游戏数据,共', count, '个');
          iview.Notice.success({
            title: `Better Steampy`,
            desc: `已从 SWI 插件加载 ${count} 个下架游戏数据`,
          });
          resolve(delistedGamesData);
        } else {
          // SWI 插件没有缓存,尝试从 API 获取
          fetchFromAPI();
        }
      });
    } else {
      // 不支持 chrome.storage,直接从 API 获取
      fetchFromAPI();
    }

    function fetchFromAPI() {
      GM_xmlhttpRequest({
        method: 'GET',
        url: 'https://steam-tracker.com/api?action=GetAppListV3',
        responseType: 'json',
        timeout: 10000,
        onload: function (response) {
          try {
            console.log('[Better Steampy] API 响应状态:', response.status);

            var data = JSON.parse(response.responseText);
            console.log('[Better Steampy] 解析后的数据结构:', Object.keys(data));

            // 转换数组格式为对象格式,方便查询
            if (data.removed_apps && Array.isArray(data.removed_apps)) {
              const removed_apps_obj = {};
              data.removed_apps.forEach(app => {
                removed_apps_obj[app.appid] = {
                  name: app.name,
                  category: app.category,
                  type: app.type
                };
              });

              const convertedData = {
                removed_apps: removed_apps_obj
              };

              GM_setValue('DelistedGamesData', convertedData);
              delistedGamesData = convertedData;
              fixBannedProfileConfirmResults();
              const count = Object.keys(removed_apps_obj).length;
              console.log('[Better Steampy] 成功加载下架游戏数据,共', count, '个');

              iview.Notice.success({
                title: `Better Steampy`,
                desc: `已加载 ${count} 个下架游戏数据`,
              });
              resolve(convertedData);
            } else {
              throw new Error('数据格式不正确');
            }
          } catch (e) {
            console.error('[Better Steampy] 解析下架游戏数据失败:', e);
            iview.Notice.error({
              title: `Better Steampy`,
              desc: `加载下架游戏数据失败: ${e.message}`,
            });
            reject(e);
          }
        },
        onerror: function(error) {
          console.error('[Better Steampy] 请求下架游戏数据失败:', error);
          iview.Notice.error({
            title: `Better Steampy`,
            desc: `请求下架游戏数据失败,请检查网络连接`,
          });
          reject(error);
        },
        ontimeout: function() {
          console.error('[Better Steampy] 请求下架游戏数据超时');
          iview.Notice.error({
            title: `Better Steampy`,
            desc: `请求下架游戏数据超时`,
          });
          reject(new Error('timeout'));
        }
      });
    }
  });
}

//获取慈善包数据(从 barter.vg)
function getBundledGamesList() {
  return new Promise((resolve, reject) => {
    console.log('[Better Steampy] 开始获取慈善包数据...');
    GM_xmlhttpRequest({
      method: 'GET',
      url: 'https://bartervg.com/browse/bundles/json/',
      responseType: 'json',
      timeout: 30000,
      onload: function (response) {
        try {
          var data = typeof response.response === 'object' ? response.response : JSON.parse(response.responseText);
          // data 格式: { "appid": { bundles: N }, ... }
          var bundleMap = {};
          var count = 0;
          for (var sku in data) {
            if (data[sku] && data[sku].bundles && data[sku].bundles > 0) {
              bundleMap[sku] = data[sku].bundles;
              count++;
            }
          }
          bundledAppsData = bundleMap;
          GM_setValue('BundledAppsData', bundleMap);
          GM_setValue('BundledAppsDataTime', Date.now());
          console.log('[Better Steampy] 成功加载慈善包数据,共', count, '个进过包的游戏');
          iview.Notice.success({
            title: 'Better Steampy',
            desc: `已加载 ${count} 个进过慈善包的游戏数据`,
          });
          resolve(bundleMap);
        } catch (e) {
          console.error('[Better Steampy] 解析慈善包数据失败:', e);
          reject(e);
        }
      },
      onerror: function(error) {
        console.error('[Better Steampy] 请求慈善包数据失败:', error);
        reject(error);
      },
      ontimeout: function() {
        console.error('[Better Steampy] 请求慈善包数据超时');
        reject(new Error('timeout'));
      }
    });
  });
}

//初始化脚本配置菜单
function init() {
  const settings = document.createElement('div');
  settings.innerHTML = `
      <div id="settings" class="ml-20-rem">
      <div class="withdraw" @click="modal=true, updateValues()">脚本设置</div>
        <Modal v-model="modal" width="980" class-name="bsp-settings-modal" footer-hide :styles="modalStyles" transition-names="">
          <div class="bsp-settings-shell">
            <div class="bsp-settings-head">
              <div>
                <h2>Better SteamPYPlus</h2>
                <p>本地库、标记和页面优化集中管理</p>
              </div>
              <div class="bsp-head-actions">
                <i-Button type="primary" size="small" @click="reloadSaves" :loading="refershSaves_loading">同步存档</i-Button>
                <i-Button size="small" @click="clearSaves">清除存档</i-Button>
              </div>
            </div>
            <div class="bsp-settings-grid">
              <section class="bsp-panel bsp-owned-panel">
                <div class="bsp-panel-title">
                  <h3>拥有状态</h3>
                  <span>24h 自动更新</span>
                </div>
                <div class="bsp-last-update">上次更新:<i-time :time="lastUpdateTime" :interval="1"></i-time></div>
                <div class="bsp-mini-stats">
                  <div><span>库存+DLC</span><strong>{{ownedApps}}</strong></div>
                  <div><span>愿望单</span><strong>{{wishlist}}</strong></div>
                  <div><span>家庭库</span><strong>{{familygameList}}</strong></div>
                </div>
                <label class="bsp-switch-line">
                  <span class="bsp-switch-label">已加入家庭组</span>
                  <i-Switch v-model="isInFamilyGroup" @on-change="isInFamilyGroup_change" />
                </label>
                <p class="bsp-muted">暂不支持捆绑包标记</p>
              </section>

              <section class="bsp-panel bsp-profile-panel">
                <div class="bsp-panel-title">
                  <h3>受限标记</h3>
                  <span>本地自维护</span>
                </div>
                <p class="bsp-muted">加一判断只使用本地确认库;自动扫描开启后,新 App 会后台抓取 Steam 商店页确认。</p>
                <div class="bsp-profile-controls">
                  <label class="bsp-switch-line">
                    <span class="bsp-switch-label">受限游戏标注</span>
                    <i-Switch v-model="checkIsProfileFeatureLimited" @on-change="checkIsProfileFeatureLimited_change" />
                  </label>
                  <label class="bsp-switch-line">
                    <span class="bsp-switch-label">自动扫描</span>
                    <i-Switch v-model="profileConfirmAutoScan" @on-change="profileConfirmAutoScan_change" />
                    <em :class="profileConfirmScanning ? 'bsp-state bsp-state-running' : 'bsp-state'">{{profileConfirmScanning ? '扫描中' : '空闲'}}</em>
                  </label>
                </div>
                <div class="bsp-profile-stats">
                  <div class="bsp-stat"><span>本地库</span><strong>{{profileConfirmTotal}}</strong></div>
                  <div class="bsp-stat bsp-stat-ok"><span>可加一</span><strong>{{profileConfirmAddable}}</strong></div>
                  <div class="bsp-stat bsp-stat-dlc"><span>DLC</span><strong>{{profileConfirmDlc}}</strong></div>
                  <div class="bsp-stat bsp-stat-nongame"><span>非游戏</span><strong>{{profileConfirmNonGame}}</strong></div>
                  <div class="bsp-stat bsp-stat-bad"><span>受限</span><strong>{{profileConfirmRestricted}}</strong></div>
                  <div class="bsp-stat bsp-stat-warn"><span>了解中</span><strong>{{profileConfirmLearning}}</strong></div>
                  <div class="bsp-stat bsp-stat-banned"><span>封禁</span><strong>{{profileConfirmBanned}}</strong></div>
                  <div class="bsp-stat bsp-stat-pending"><span>待扫描</span><strong>{{profileConfirmQueued}}</strong></div>
                </div>
                <div class="bsp-profile-actions">
                  <i-Button type="primary" size="small" @click="startProfileConfirmScan" :loading="profileConfirmScanning">开始扫描</i-Button>
                  <i-Button size="small" @click="stopProfileConfirmScan">停止</i-Button>
                  <i-Button size="small" @click="reloadLimitedSaves" :loading="reloadLimitedSaves_loading">收集当前页</i-Button>
                  <i-Button size="small" @click="exportProfileConfirmSaves">导出</i-Button>
                  <i-Button size="small" @click="importProfileConfirmSaves">导入</i-Button>
                  <i-Button type="error" ghost size="small" @click="clearProfileConfirmSaves">清空</i-Button>
                </div>
              </section>

              <section class="bsp-panel bsp-color-panel">
                <div class="bsp-panel-title"><h3>标记颜色</h3></div>
                <div class="bsp-color-grid">
                  <label>已拥有 <Color-Picker v-model="ownedAppsColor" size="small" :colors="defaultcolors" @on-change="ownedAppsColor_change" /></label>
                  <label>愿望单 <Color-Picker v-model="wishlistColor" size="small" :colors="defaultcolors" @on-change="wishlistColor_change" /></label>
                  <label>家庭库 <Color-Picker v-model="familygameColor" size="small" :colors="defaultcolors" @on-change="familygameColor_change" /></label>
                  <label>未拥有 <Color-Picker v-model="unownedColor" size="small" :colors="defaultcolors" @on-change="unownedColor_change" /></label>
                </div>
              </section>

              <section class="bsp-panel bsp-page-panel">
                <div class="bsp-panel-title"><h3>网页优化</h3></div>
                <label class="bsp-switch-line">
                  <span class="bsp-switch-label">隐藏右下推广侧栏</span>
                  <i-Switch v-model="isSuspensionOff" @on-change="isSuspensionOff_change" />
                </label>
              </section>

              <section class="bsp-panel bsp-blacklist-panel">
                <div class="bsp-panel-title">
                  <h3>黑名单</h3>
                  <span>{{blacklistCount}} 个</span>
                </div>
                <p class="bsp-muted">点击游戏卡片上的禁用按钮即可加入黑名单。</p>
                <div v-if="blacklistItems.length > 0" class="bsp-blacklist-list">
                  <div v-for="item in blacklistItems" :key="item.appId" class="bsp-blacklist-item">
                    <span>
                      <a :href="'https://store.steampowered.com/app/' + item.appId" target="_blank">{{item.name}}</a>
                      <em>{{item.appId}}</em>
                    </span>
                    <i-Button size="small" type="error" ghost @click="removeBlacklistItem(item.appId)">移除</i-Button>
                  </div>
                </div>
                <div v-else class="bsp-empty">黑名单为空</div>
                <i-Button type="error" ghost size="small" @click="clearBlacklist" v-if="blacklistItems.length > 0">清空黑名单</i-Button>
              </section>
            </div>
          </div>
        </Modal>
      </div>
  `;
  const filter = document.createElement('div');
  filter.innerHTML = `
  <div id="filter" class="ml-20-rem">
        <div id="filter" style="position: relative; display: flex; align-items: center; flex-wrap: wrap; gap: 6px;">
          <span style="color:#515a6e;font-size:13px;margin-right:4px;font-weight:500;">筛选:</span>
          <Checkbox-Group v-model="filter" @on-change="filterChange" style="display:inline-flex;align-items:center;flex-wrap:wrap;gap:6px;">
            <Checkbox label="noOwnedGames"><Tag :color="filter.includes('noOwnedGames')?'primary':'default'" size="medium" style="cursor:pointer;margin:0;font-size:13px;padding:0 12px;height:28px;line-height:28px;">隐藏已拥有</Tag></Checkbox>
            <Checkbox label="noRestrictedGames"><Tag :color="filter.includes('noRestrictedGames')?'primary':'default'" size="medium" style="cursor:pointer;margin:0;font-size:13px;padding:0 12px;height:28px;line-height:28px;">隐藏受限</Tag></Checkbox>
            <Checkbox label="noDlc"><Tag :color="filter.includes('noDlc')?'primary':'default'" size="medium" style="cursor:pointer;margin:0;font-size:13px;padding:0 12px;height:28px;line-height:28px;">隐藏DLC</Tag></Checkbox>
            <Checkbox label="noBlacklistGames"><Tag :color="filter.includes('noBlacklistGames')?'primary':'default'" size="medium" style="cursor:pointer;margin:0;font-size:13px;padding:0 12px;height:28px;line-height:28px;">隐藏黑名单</Tag></Checkbox>
            <Checkbox label="noBundledGames"><Tag :color="filter.includes('noBundledGames')?'success':'default'" size="medium" style="cursor:pointer;margin:0;font-size:13px;padding:0 12px;height:28px;line-height:28px;">隐藏慈善包</Tag></Checkbox>
            <Checkbox label="scanHideAllGames"><Tag :color="filter.includes('scanHideAllGames')?'error':'default'" size="medium" style="cursor:pointer;margin:0;font-size:13px;padding:0 12px;height:28px;line-height:28px;">全部隐藏</Tag></Checkbox>
            <div style="display: inline-flex; align-items: center; position: relative;">
              <Checkbox label="onlyDelistedGames"><Tag :color="filter.includes('onlyDelistedGames')?'warning':'default'" size="medium" style="cursor:pointer;margin:0;font-size:13px;padding:0 12px;height:28px;line-height:28px;">仅下架</Tag></Checkbox>
              <span
                v-if="showDelistedTypes"
                @click.stop.prevent="toggleDelistedTypesPanel"
                style="cursor:pointer;padding:0 4px;transition:transform 0.2s;display:inline-block;user-select:none;font-size:10px;color:#808695;"
                :style="{ transform: delistedTypesPanelExpanded ? 'rotate(90deg)' : 'rotate(0deg)' }">▼</span>
              <div v-if="showDelistedTypes && delistedTypesPanelExpanded"
                   style="position:absolute;left:100%;top:-4px;margin-left:6px;background:#fff;border:1px solid #dcdee2;border-radius:4px;padding:6px 10px;box-shadow:0 2px 8px rgba(0,0,0,0.15);z-index:1000;min-width:160px;">
                <Checkbox-Group v-model="delistedTypes" @on-change="delistedTypesChange" style="display:flex;flex-direction:column;gap:4px;font-size:12px;">
                  <Checkbox label="purchase disabled" style="margin:0;">Purchase Disabled</Checkbox>
                  <Checkbox label="delisted" style="margin:0;">Delisted</Checkbox>
                  <Checkbox label="f2p" style="margin:0;">F2P</Checkbox>
                  <Checkbox label="unreleased" style="margin:0;">Unreleased</Checkbox>
                  <Checkbox label="test app" style="margin:0;">Test App</Checkbox>
                  <Checkbox label="retail only" style="margin:0;">Retail Only</Checkbox>
                  <Checkbox label="pre-order exclusive" style="margin:0;">Pre-order</Checkbox>
                  <Checkbox label="banned" style="margin:0;">Banned</Checkbox>
                </Checkbox-Group>
              </div>
            </div>
          </Checkbox-Group>
      `;
  const targetElement = document.querySelector('.balanceTitle > div');
  targetElement.appendChild(settings);
  targetElement.appendChild(filter);
  settingsVm = new Vue({
    el: '#settings',
    data() {
      return {
        reloadLimitedSaves_loading: false,
        refershSaves_loading: false,
        modal: false,
        modalStyles: {
          top: '50%',
          transform: 'translateY(-50%)'
        },
        lastUpdateTime: Saves.lastupdatetime,
        ownedApps: Saves.ownedApps.length,
        wishlist: Saves.wishlist.length,
        familygameList: Saves.familygameList.length,
        limitedApps: limitedApps.length,
        profileConfirmTotal: getProfileConfirmStats().total,
        profileConfirmAddable: limitedApps.length,
        profileConfirmNormal: getProfileConfirmStats().normal,
        profileConfirmRestricted: getProfileConfirmStats().restricted,
        profileConfirmLearning: getProfileConfirmStats().learning,
        profileConfirmBanned: getProfileConfirmStats().banned,
        profileConfirmDlc: getProfileConfirmStats().dlc,
        profileConfirmNonGame: getProfileConfirmStats().nonGame,
        profileConfirmUnknown: getProfileConfirmStats().unknown,
        profileConfirmQueued: getProfileConfirmStats().queued,
        profileConfirmScanning: profileConfirmScanning,
        profileConfirmAutoScan: profileConfirmAutoScan,
        isInFamilyGroup: JSON.parse(localStorage.getItem('isInfamily')),
        checkIsProfileFeatureLimited: JSON.parse(
          localStorage.getItem('IsProfileFeatureLimited')
        ),
        isSuspensionOff: JSON.parse(localStorage.getItem('isSuspensionOff')),
        ownedAppsColor: localStorage.getItem('ownedColor'),
        wishlistColor: localStorage.getItem('wishlistColor'),
        familygameColor: localStorage.getItem('familygameColor'),
        unownedColor: localStorage.getItem('unownedColor'),
        defaultcolors: ['#0c8918', '#177cb0', '#ff8936', '#ff2e63'],
        blacklistItems: [...blacklist],
        blacklistCount: blacklist.length,
      };
    },
    methods: {
      updateValues() {
        this.ownedApps = Saves.ownedApps.length;
        this.wishlist = Saves.wishlist.length;
        this.familygameList = Saves.familygameList.length;
        this.limitedApps = limitedApps.length;
        this.profileConfirmTotal = getProfileConfirmStats().total;
        this.profileConfirmAddable = limitedApps.length;
        this.lastUpdateTime = Saves.lastupdatetime;
        this.blacklistItems = [...blacklist];
        this.blacklistCount = blacklist.length;
        updateProfileConfirmStats();
      },
      isInFamilyGroup_change(status) {
        if (status) {
          localStorage.setItem('isInfamily', JSON.stringify(true));
        } else {
          localStorage.removeItem('isInfamily');
        }
      },
      checkIsProfileFeatureLimited_change(status) {
        if (status) {
          localStorage.setItem('IsProfileFeatureLimited', JSON.stringify(true));
          Saves = GM_getValue('Saves');
          queueAllCdkeyGameCheckers();
          checkAndAutoSkip(); // 添加自动跳页检测
        } else {
          localStorage.removeItem('IsProfileFeatureLimited');
          const elements = document.querySelectorAll('.ProfileFeaturesLimited');
          elements.forEach((element) => {
            element.parentNode.removeChild(element);
          });
        }
      },
      isSuspensionOff_change(status) {
        if (status) {
          localStorage.setItem('isSuspensionOff', JSON.stringify(true));
          GM_addStyle('.suspension{display:none}');
        } else {
          GM_addStyle('.suspension{display:block}');
          localStorage.removeItem('isSuspensionOff');
        }
      },
      ownedAppsColor_change(color) {
        ownedColor = color;
        localStorage.setItem('ownedColor', color);
        queueAllCdkeyGameCheckers();
      },
      wishlistColor_change(color) {
        wishlistColor = color;
        localStorage.setItem('wishlistColor', color);
        queueAllCdkeyGameCheckers();
      },
      familygameColor_change(color) {
        familygameColor = color;
        localStorage.setItem('familygameColor', color);
        queueAllCdkeyGameCheckers();
      },
      unownedColor_change(color) {
        unownedColor = color;
        localStorage.setItem('unownedColor', color);
        queueAllCdkeyGameCheckers();
      },
      async reloadSaves() {
        this.$Notice.info({
          title: '正在重载存档',
        });
        this.refershSaves_loading = true;
        await Promise.all([
          getOwnAndWish(),
          this.isInFamilyGroup ? getFamilyGame() : Promise.resolve(),
        ]);
        Saves = GM_getValue('Saves');
        saveProfileConfirmDataNow();
        queueAllCdkeyGameCheckers();
        this.updateValues();
        this.refershSaves_loading = false;
        this.$Notice.success({
          title: '重载完毕',
        });
        checkAndAutoSkip(); // 添加自动跳页检测
      },
      async reloadLimitedSaves() {
        this.$Notice.info({
          title: '正在收集当前页 App',
        });
        this.reloadLimitedSaves_loading = true;
        await getLimitedGamesList();
        this.updateValues();
        this.reloadLimitedSaves_loading = false;
        this.$Notice.success({
          title: '收集完毕',
        });
        checkAndAutoSkip(); // 添加自动跳页检测
      },
      profileConfirmAutoScan_change(status) {
        profileConfirmAutoScan = !!status;
        GM_setValue(PROFILE_CONFIRM_AUTO_SCAN_KEY, profileConfirmAutoScan);
        updateProfileConfirmStats();
        if (profileConfirmAutoScan) {
          scheduleProfileConfirmScan(200);
        } else {
          abortProfileConfirmScan();
        }
      },
      startProfileConfirmScan() {
        profileConfirmAutoScan = true;
        GM_setValue(PROFILE_CONFIRM_AUTO_SCAN_KEY, true);
        updateProfileConfirmStats();
        runProfileConfirmScan(10000);
      },
      stopProfileConfirmScan() {
        profileConfirmAutoScan = false;
        GM_setValue(PROFILE_CONFIRM_AUTO_SCAN_KEY, false);
        abortProfileConfirmScan();
        this.$Notice.info({ title: '已停止本地库扫描' });
      },
      exportProfileConfirmSaves() {
        exportProfileConfirmData();
        this.$Notice.success({ title: '本地状态库已导出' });
      },
      importProfileConfirmSaves() {
        importProfileConfirmDataFromFile();
      },
      clearProfileConfirmSaves() {
        clearProfileConfirmData();
        this.$Notice.success({ title: '本地状态库已清空' });
        queueAllCdkeyGameCheckers();
      },
      clearSaves() {
        this.$Notice.info({
          title: '存档已清除',
        });
        let nullSaves = {
          wishlist: [],
          ownedApps: [],
          familygameList: [],
          lastupdatetime: 0,
        };
        Saves = nullSaves;
        GM_setValue('Saves', nullSaves);
        this.updateValues();
      },
      removeBlacklistItem(appId) {
        removeFromBlacklist(appId);
        this.blacklistItems = [...blacklist];
        this.blacklistCount = blacklist.length;
        // 刷新页面显示
        queueAllCdkeyGameCheckers();
      },
      clearBlacklist() {
        blacklist = [];
        GM_setValue('Blacklist', blacklist);
        this.blacklistItems = [];
        this.blacklistCount = 0;
        queueAllCdkeyGameCheckers();
        this.$Notice.success({ title: '黑名单已清空' });
      },
    },
  });
  new Vue({
    el: '#filter',
    data() {
      return {
        filter: [],
        delistedTypes: ['purchase disabled', 'delisted', 'f2p', 'unreleased', 'test app', 'retail only', 'pre-order exclusive', 'banned'],
        showDelistedTypes: false,
        delistedTypesPanelExpanded: false,
      };
    },
    computed: {},
    methods: {
      filterChange() {
        noownedGames = this.filter.includes('noOwnedGames');
        noRestrictedGames = this.filter.includes('noRestrictedGames');
        noDlc = this.filter.includes('noDlc');
        noBlacklistGames = this.filter.includes('noBlacklistGames');
        noBundledGames = this.filter.includes('noBundledGames');
        scanHideAllGames = this.filter.includes('scanHideAllGames');
        onlyDelistedGames = this.filter.includes('onlyDelistedGames');
        if (onlyDelistedGames && (!delistedGamesData || !delistedGamesData.removed_apps)) {
          getDelistedGamesList().then(() => {
            queueAllCdkeyGameCheckers();
            checkAndAutoSkip();
          }).catch(() => {});
        }
        if (noBundledGames && !bundledAppsData) {
          getBundledGamesList().then(() => {
            queueAllCdkeyGameCheckers();
            checkAndAutoSkip();
          }).catch(() => {});
        }
        this.showDelistedTypes = onlyDelistedGames;
        if (onlyDelistedGames) {
          this.delistedTypesPanelExpanded = true;
        }
        delistedTypes = this.delistedTypes;
        queueAllCdkeyGameCheckers();
        checkAndAutoSkip();
      },
      delistedTypesChange() {
        delistedTypes = this.delistedTypes;
        queueAllCdkeyGameCheckers();
        checkAndAutoSkip();
      },
      toggleDelistedTypesPanel() {
        this.delistedTypesPanelExpanded = !this.delistedTypesPanelExpanded;
      },
    },
  });
  if (localStorage.getItem('isSuspensionOff') === 'true') {
    GM_addStyle('.suspension{display:none}');
  }
}

//游戏状态标记-CDKEY
function cdkeyGameChecker(element) {
  const isAppOwned = (appId) => Saves.ownedApps.includes(appId);
  const isAppinwishlist = (appId) => Saves.wishlist.includes(appId);
  const isAppShared = (appId) => Saves.familygameList.includes(appId);
  const isNotLimited = (appId) => isProfileRestrictedByConfirmedStatus(appId);
  const isDLC = (appId) => isNoGameApp(appId);
  const getAppId = (url) => (url.match(/\/apps\/(\d+)\//) || [])[1] || null;
  const getBundleId = (url) =>(url.match(/\/bundles\/(\d+)\//) || [])[1] || null;
  const appId = Number(getAppId(element.getAttribute('data-src')));
  const gameNameElement = element
    .closest('.gameblock')
    .querySelector('.gameName');
  const gameBlock = element.closest('.gameblock');

  // 检测游戏是否下架以及下架类型(使用 Steam-Tracker 数据)
  const getDelistedType = () => {
    if (!delistedGamesData || !delistedGamesData.removed_apps) {
      return null;
    }

    const appIdStr = appId.toString();
    const gameData = delistedGamesData.removed_apps[appIdStr];

    if (!gameData) {
      // 不是下架游戏
      return null;
    }

    // Steam-Tracker 的 category 字段对应下架类型
    const category = gameData.category;
    if (!category) {
      return null;
    }

    // 映射 Steam-Tracker 的 category 到我们的类型
    const categoryMap = {
      'Purchase disabled': 'purchase disabled',
      'Delisted': 'delisted',
      'Delisted video': 'delisted',
      'F2P': 'f2p',
      'Unreleased': 'unreleased',
      'Test app': 'test app',
      'Retail only': 'retail only',
      'Pre-order exclusive': 'pre-order exclusive',
      'Banned': 'banned'
    };

    const mappedType = categoryMap[category] || category.toLowerCase();
    return mappedType;
  };

  if (appId != 0) {
    rememberProfileConfirmCandidate(appId);
    element.parentElement.parentElement.style.display = 'block';

    // 添加黑名单按钮(如果还没有)
    if (gameBlock && !gameBlock.querySelector('.bsp-blacklist-btn')) {
      const btn = document.createElement('div');
      btn.className = 'bsp-blacklist-btn';
      btn.title = isInBlacklist(appId) ? '已在黑名单(点击移除)' : '加入黑名单';
      btn.textContent = isInBlacklist(appId) ? '✅' : '🚫';
      btn.style.cssText = 'position:absolute;top:4px;left:4px;z-index:100;cursor:pointer;font-size:18px;line-height:1;background:rgba(0,0,0,0.5);border-radius:50%;width:26px;height:26px;display:flex;align-items:center;justify-content:center;opacity:0.7;transition:opacity 0.2s;';
      btn.onmouseenter = () => { btn.style.opacity = '1'; };
      btn.onmouseleave = () => { btn.style.opacity = '0.7'; };
      btn.onclick = (e) => {
        e.stopPropagation();
        e.preventDefault();
        const gameName = gameNameElement ? gameNameElement.textContent.trim() : '';
        if (isInBlacklist(appId)) {
          removeFromBlacklist(appId);
          btn.textContent = '🚫';
          btn.title = '加入黑名单';
          iview.Notice.info({ title: 'Better Steampy', desc: `已将「${gameName}」移出黑名单`, duration: 2 });
        } else {
          addToBlacklist(appId, gameName);
          btn.textContent = '✅';
          btn.title = '已在黑名单(点击移除)';
          iview.Notice.info({ title: 'Better Steampy', desc: `已将「${gameName}」加入黑名单`, duration: 2 });
        }
        // 如果启用了黑名单筛选,立即刷新显示
        if (noBlacklistGames) {
          queueAllCdkeyGameCheckers();
          checkAndAutoSkip();
        }
      };
      // 插入到 gameBlock 中(相对定位的容器)
      const imgWrap = gameBlock.querySelector('.cdkGameImg') || gameBlock;
      imgWrap.style.position = 'relative';
      imgWrap.appendChild(btn);
    } else if (gameBlock) {
      // 更新已有按钮状态
      const existingBtn = gameBlock.querySelector('.bsp-blacklist-btn');
      if (existingBtn) {
        existingBtn.textContent = isInBlacklist(appId) ? '✅' : '🚫';
        existingBtn.title = isInBlacklist(appId) ? '已在黑名单(点击移除)' : '加入黑名单';
      }
    }

    if (scanHideAllGames) {
      element.parentElement.parentElement.style.display = 'none';
      return;
    }

    // 黑名单筛选
    if (noBlacklistGames && isInBlacklist(appId)) {
      element.parentElement.parentElement.style.display = 'none';
      return;
    }

    // 慈善包筛选
    if (noBundledGames && bundledAppsData) {
      const bundleCount = bundledAppsData[appId.toString()];
      if (bundleCount && bundleCount > 0) {
        element.parentElement.parentElement.style.display = 'none';
        return;
      }
    }

    // 下架游戏筛选逻辑
    if (onlyDelistedGames) {
      const delistedType = getDelistedType();
      if (!delistedType || !delistedTypes.includes(delistedType)) {
        // 不是下架游戏,或者不在选中的下架类型中,隐藏
        element.parentElement.parentElement.style.display = 'none';
        return;
      }
    }

    if (noDlc) {
      if (isDLC(appId)) {
        element.parentElement.parentElement.style.display = 'none';
      }
    }
    if (isAppOwned(appId)) {
      if (noownedGames) {
        element.parentElement.parentElement.style.display = 'none';
      } else {
        gameNameElement.style.color = ownedColor;
      }
    } else if (isAppShared(appId)) {
      gameNameElement.style.color = familygameColor;
    } else if (isAppinwishlist(appId)) {
      gameNameElement.style.color = wishlistColor;
    } else {
      gameNameElement.style.color = unownedColor;
    }
    if (localStorage.getItem('IsProfileFeatureLimited')) {
      const existingDiscountDiv = element.parentElement.querySelector(
        '.ProfileFeaturesLimited'
      );
      if (existingDiscountDiv) {
        existingDiscountDiv.remove();
      }
      // 恢复使用 limitedApps 列表判断(不在列表中=受限)
      if (isNotLimited(appId)) {
        if (noRestrictedGames) {
          element.parentElement.parentElement.style.display = 'none';
        } else {
          const discountDiv = document.createElement('div');
          discountDiv.className = 'ProfileFeaturesLimited';
          discountDiv.textContent = '资料受限';
          element.parentElement.appendChild(discountDiv);
        }
      }
    }

  }
}

//加载存档
function load() {
  var previousSave = GM_getValue('Saves');
  if (previousSave !== undefined) {
    Saves = GM_getValue('Saves');
  } else {
    GM_setValue('Saves', Saves);
  }
  var previousProfileConfirmDB = GM_getValue(PROFILE_CONFIRM_DB_KEY);
  if (previousProfileConfirmDB !== undefined && previousProfileConfirmDB && typeof previousProfileConfirmDB === 'object') {
    profileConfirmDB = previousProfileConfirmDB;
  }
  rebuildLocalLimitedApps();
  var previousProfileConfirmQueue = GM_getValue(PROFILE_CONFIRM_QUEUE_KEY);
  if (Array.isArray(previousProfileConfirmQueue)) {
    profileConfirmQueue = previousProfileConfirmQueue.map(Number).filter(id => id > 0);
    normalizeProfileConfirmQueue();
  }
  var previousProfileConfirmAutoScan = GM_getValue(PROFILE_CONFIRM_AUTO_SCAN_KEY);
  var profileConfirmAutoScanMigrated = GM_getValue(PROFILE_CONFIRM_AUTO_SCAN_MIGRATED_KEY);
  if (!profileConfirmAutoScanMigrated) {
    profileConfirmAutoScan = false;
    GM_setValue(PROFILE_CONFIRM_AUTO_SCAN_KEY, false);
    GM_setValue(PROFILE_CONFIRM_AUTO_SCAN_MIGRATED_KEY, true);
  } else if (previousProfileConfirmAutoScan !== undefined) {
    profileConfirmAutoScan = !!previousProfileConfirmAutoScan;
  } else {
    GM_setValue(PROFILE_CONFIRM_AUTO_SCAN_KEY, profileConfirmAutoScan);
  }
  rebuildLocalNoGameList();
  normalizeProfileConfirmQueue();
  var previousDelistedGamesData = GM_getValue('DelistedGamesData');
  if (previousDelistedGamesData !== undefined && previousDelistedGamesData !== null && previousDelistedGamesData.removed_apps) {
    delistedGamesData = previousDelistedGamesData;
    fixBannedProfileConfirmResults();
  }
  // 加载黑名单
  var previousBlacklist = GM_getValue('Blacklist');
  if (previousBlacklist !== undefined && Array.isArray(previousBlacklist)) {
    blacklist = previousBlacklist;
    console.log('[Better Steampy] 从缓存加载黑名单,共', blacklist.length, '个');
  }
  // 加载慈善包数据
  var previousBundledAppsData = GM_getValue('BundledAppsData');
  if (previousBundledAppsData !== undefined && previousBundledAppsData !== null && typeof previousBundledAppsData === 'object') {
    bundledAppsData = previousBundledAppsData;
  }
  //自动更新
  if (new Date().getTime() - Saves.lastupdatetime > 86400000) {
    iview.Notice.info({
      title: '存档自动更新中',
    });
    getOwnAndWish();
    if (JSON.parse(localStorage.getItem('isInfamily'))) {
      getFamilyGame();
    }
    rebuildLocalLimitedApps();
    rebuildLocalNoGameList();
  }
  ensureProfileConfirmAutoScan(1000);
}

//监听页面变化
function observePageChanges() {
  const config = {
    childList: true,
    subtree: true,
    attributes: true,
    attributeFilter: ['data-src'],
  };
  let hasExecuted = false;
  let pageChangeTimer = null; // 用于检测页面数据加载完成
  let paginationListenerAdded = false; // 是否已添加分页监听

  const callback = function (mutationsList, observer) {
    let hasGameIconChange = false;

    for (let mutation of mutationsList) {
      if (
        mutation.type === 'attributes' &&
        mutation.attributeName === 'data-src'
      ) {
        const targetElement = mutation.target;
        if (targetElement.classList.contains('cdkGameIcon')) {
          queueCdkeyGameChecker(targetElement);
          hasGameIconChange = true;
        }
      }

      if (mutation.type === 'childList') {
        for (let node of mutation.addedNodes) {
          if (node.nodeType !== 1) continue;
          if (node.classList && node.classList.contains('cdkGameIcon')) {
            queueCdkeyGameChecker(node);
            hasGameIconChange = true;
          } else if (node.querySelectorAll) {
            const icons = node.querySelectorAll('.cdkGameIcon');
            if (icons.length > 0) {
              icons.forEach((icon) => queueCdkeyGameChecker(icon));
              hasGameIconChange = true;
            }
          }
        }
      }

      if (!hasExecuted && mutation.type === 'childList') {
        const balanceTitleElement = document.querySelector(
          '.balanceTitle > div'
        );
        if (balanceTitleElement) {
          init();
          hasExecuted = true;
        }
      }
    }

    // 添加分页按钮点击监听(只添加一次)
    if (!paginationListenerAdded) {
      const pagination = document.querySelector('.ivu-page');
      if (pagination) {
        pagination.addEventListener('click', function(e) {
          // 检测是否点击了分页按钮(不是由脚本触发的自动翻页)
          if (!isAutoSkipping) {
            resetAutoSkipState();
          }
        });
        paginationListenerAdded = true;
      }
    }

    // 如果有游戏图标变化,说明页面数据在加载
    if (hasGameIconChange) {
      if (pageChangeTimer) {
        clearTimeout(pageChangeTimer);
      }

      // 等待游戏数据加载完成后检测
      pageChangeTimer = setTimeout(() => {
        // 只有在启用了过滤选项时才检测自动跳页
        if (noownedGames || noRestrictedGames || noDlc || onlyDelistedGames || noBlacklistGames || noBundledGames || scanHideAllGames) {
          checkAndAutoSkip();
        }
      }, 800); // 减少等待时间,因为不需要等待 SWI 插件渲染
    }

  };

  const observer = new MutationObserver(callback);
  observer.observe(document.body, config);
}
//CSS样式
const style = document.createElement('style');
style.innerHTML = `
    .ProfileFeaturesLimited {
    height: .26rem;
    background: #ed4014;
    position: absolute;
    top: 4px;
    left: 34px;
    color: #fff;
    text-align: center;
    line-height: .26rem;
    font-size: .11rem;
    padding: 0 6px;
    border-radius: 3px;
    white-space: nowrap;
    z-index: 99;
}
#filter > .ivu-checkbox-group > .ivu-checkbox-wrapper {
    margin-right: 0 !important;
    font-size: 0;
}
#filter > .ivu-checkbox-group > div > .ivu-checkbox-wrapper {
    margin-right: 0 !important;
    font-size: 0;
}
#filter > .ivu-checkbox-group > .ivu-checkbox-wrapper > .ivu-checkbox,
#filter > .ivu-checkbox-group > div > .ivu-checkbox-wrapper > .ivu-checkbox {
    display: none !important;
}
#filter > .ivu-checkbox-group > .ivu-checkbox-wrapper > .ivu-checkbox + span,
#filter > .ivu-checkbox-group > div > .ivu-checkbox-wrapper > .ivu-checkbox + span {
    padding: 0 !important;
}
#filter .ivu-tag-default {
    border: 1px solid #b8becc !important;
    color: #515a6e !important;
    background: #f8f8f9 !important;
}
#filter .ivu-tag-default:hover {
    border-color: #2d8cf0 !important;
    color: #2d8cf0 !important;
}
#filter .ivu-tag-primary {
    box-shadow: 0 1px 4px rgba(45,140,240,0.3);
}
#filter .ivu-tag-warning {
    box-shadow: 0 1px 4px rgba(255,153,0,0.3);
}
.bsp-settings-modal {
    align-items: center;
    justify-content: center;
}
.bsp-settings-modal .ivu-modal {
    margin: 0 auto;
}
.bsp-settings-modal .ivu-modal-content {
    border-radius: 8px;
    overflow: visible;
}
.bsp-settings-modal .ivu-modal-body {
    padding: 16px;
    max-height: calc(100vh - 72px);
    overflow: visible;
    background: #f5f7fa;
}
.bsp-settings-shell {
    color: #344054;
}
.bsp-settings-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    margin-bottom: 12px;
    padding: 2px 4px 12px;
    border-bottom: 1px solid #e8eaec;
}
.bsp-settings-head h2 {
    margin: 0;
    color: #17233d;
    font-size: 22px;
    line-height: 1.2;
}
.bsp-settings-head p {
    margin: 4px 0 0;
    color: #808695;
    font-size: 13px;
}
.bsp-head-actions {
    display: flex;
    align-items: center;
    gap: 8px;
}
.bsp-settings-grid {
    display: grid;
    grid-template-columns: 330px minmax(0, 1fr);
    grid-template-areas:
        "owned profile"
        "color blacklist"
        "page blacklist";
    gap: 10px;
}
.bsp-panel {
    min-width: 0;
    padding: 12px;
    border: 1px solid #e4e7ed;
    border-radius: 8px;
    background: #fff;
}
.bsp-owned-panel { grid-area: owned; }
.bsp-profile-panel { grid-area: profile; }
.bsp-color-panel { grid-area: color; }
.bsp-page-panel { grid-area: page; }
.bsp-blacklist-panel { grid-area: blacklist; }
.bsp-panel-title {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    margin-bottom: 10px;
}
.bsp-panel-title h3 {
    margin: 0;
    color: #17233d;
    font-size: 16px;
    line-height: 1.2;
}
.bsp-panel-title span {
    flex: none;
    padding: 2px 8px;
    border-radius: 999px;
    background: #f0f7ff;
    color: #2d8cf0;
    font-size: 12px;
}
.bsp-muted,
.bsp-last-update {
    margin: 0 0 10px;
    color: #808695;
    font-size: 13px;
    line-height: 1.45;
}
.bsp-mini-stats {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 8px;
    margin-bottom: 8px;
}
.bsp-mini-stats div {
    padding: 8px;
    border-radius: 6px;
    background: #f8f8f9;
}
.bsp-mini-stats span {
    display: block;
    color: #808695;
    font-size: 12px;
}
.bsp-mini-stats strong {
    display: block;
    margin-top: 2px;
    color: #17233d;
    font-size: 20px;
    line-height: 1.2;
}
.bsp-color-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 8px 10px;
}
.bsp-color-grid label {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    min-height: 30px;
    color: #515a6e;
}
.bsp-color-panel {
    overflow: visible;
}
.bsp-color-grid .ivu-color-picker-picker {
    z-index: 3000 !important;
}
.bsp-blacklist-list {
    max-height: 132px;
    overflow-y: auto;
    margin-bottom: 8px;
    border: 1px solid #eef0f3;
    border-radius: 6px;
}
.bsp-blacklist-item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    padding: 6px 8px;
    border-bottom: 1px solid #eef0f3;
}
.bsp-blacklist-item:last-child {
    border-bottom: 0;
}
.bsp-blacklist-item span {
    min-width: 0;
}
.bsp-blacklist-item a {
    display: inline-block;
    max-width: 250px;
    overflow: hidden;
    text-overflow: ellipsis;
    vertical-align: bottom;
    white-space: nowrap;
    color: #2d8cf0;
}
.bsp-blacklist-item em {
    margin-left: 6px;
    color: #98a2b3;
    font-style: normal;
    font-size: 12px;
}
.bsp-empty {
    padding: 18px 0;
    color: #98a2b3;
    text-align: center;
}
.bsp-local-profile-card .ivu-card-body {
    padding-top: 16px;
}
.bsp-section-title {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
}
.bsp-section-title h2 {
    margin: 0;
    color: #515a6e;
}
.bsp-section-title span {
    flex: none;
    padding: 2px 8px;
    border-radius: 999px;
    background: #f0f7ff;
    color: #2d8cf0;
    font-size: 12px;
    font-weight: 500;
}
.bsp-profile-panel {
    display: flex;
    flex-direction: column;
    gap: 10px;
}
.bsp-profile-alert {
    margin-bottom: 0 !important;
}
.bsp-profile-controls {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 8px;
}
.bsp-switch-line {
    min-height: 34px;
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 6px 0;
    border-bottom: 1px solid #eef0f3;
    color: #515a6e;
}
.bsp-switch-label {
    flex: 1;
    min-width: 0;
    font-weight: 500;
}
.bsp-switch-line .ivu-switch {
    flex: none;
}
.bsp-state {
    flex: none;
    font-style: normal;
    font-size: 12px;
    color: #808695;
}
.bsp-state-running {
    color: #2d8cf0;
}
.bsp-profile-stats {
    display: grid;
    grid-template-columns: repeat(4, minmax(84px, 1fr));
    gap: 7px;
}
.bsp-stat {
    min-height: 56px;
    padding: 8px;
    border: 1px solid #e8eaec;
    border-radius: 6px;
    background: #f8f8f9;
}
.bsp-stat span {
    display: block;
    color: #808695;
    font-size: 12px;
}
.bsp-stat strong {
    display: block;
    margin-top: 2px;
    color: #17233d;
    font-size: 21px;
    line-height: 1.2;
}
.bsp-stat-ok {
    background: #f3fbf5;
    border-color: #c9ebd1;
}
.bsp-stat-bad {
    background: #fff5f2;
    border-color: #ffd8cf;
}
.bsp-stat-warn {
    background: #fff9e6;
    border-color: #ffe7a3;
}
.bsp-stat-dlc {
    background: #f0faff;
    border-color: #bfe7ff;
}
.bsp-stat-nongame {
    background: #f6f3ff;
    border-color: #d8cef8;
}
.bsp-stat-banned {
    background: #fff1f0;
    border-color: #ffbdb8;
}
.bsp-stat-pending {
    background: #f7f7fb;
    border-color: #dcdee6;
}
.bsp-profile-actions {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 8px;
    padding-top: 2px;
}
@media (max-width: 720px) {
    .bsp-settings-grid {
        grid-template-columns: 1fr;
        grid-template-areas:
            "owned"
            "profile"
            "color"
            "page"
            "blacklist";
    }
    .bsp-profile-controls {
        grid-template-columns: 1fr;
    }
    .bsp-profile-stats {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }
}
.bsp-cp-btn {
    cursor: pointer;
    padding: 4px 10px;
    border: 1px solid #dcdee2;
    border-radius: 4px;
    font-size: 13px;
    transition: all 0.15s;
}
.bsp-cp-btn:hover {
    border-color: #2d8cf0;
    color: #2d8cf0;
}
.bsp-cp-active {
    background: #2d8cf0 !important;
    color: #fff !important;
    border-color: #2d8cf0 !important;
}
`;
document.head.appendChild(style);

//默认颜色
if (!localStorage.getItem('ownedColor')) {
  localStorage.setItem('ownedColor', '#0c8918');
  localStorage.setItem('wishlistColor', '#177cb0');
  localStorage.setItem('familygameColor', '#ff8936');
  localStorage.setItem('unownedColor', '#ff2e63');
}
var ownedColor = localStorage.getItem('ownedColor');
var wishlistColor = localStorage.getItem('wishlistColor');
var familygameColor = localStorage.getItem('familygameColor');
var unownedColor = localStorage.getItem('unownedColor');