Steam Badge Enhancer

Enhances Steam badges with detailed data, crafted highlight, IndexedDB for local caching, immediate cached display, and optional manual re-queue button. Seamless data hot-swapping during background refresh.

  1. // ==UserScript==
  2. // @name Steam Badge Enhancer
  3. // @namespace https://github.com/encumber
  4. // @version 2.1
  5. // @description Enhances Steam badges with detailed data, crafted highlight, IndexedDB for local caching, immediate cached display, and optional manual re-queue button. Seamless data hot-swapping during background refresh.
  6. // @author Nitoned
  7. // @match https://steamcommunity.com/*/badges/*
  8. // @match https://steamcommunity.com/*/badges*
  9. // @grant GM_xmlhttpRequest
  10. // @grant GM_addStyle
  11. // @grant GM_getValue
  12. // @grant GM_setValue
  13. // ==/UserScript==
  14.  
  15. (function() {
  16. 'use strict';
  17.  
  18. // --- Configuration ---
  19. const STEAMSETS_API_KEY = ''; // Get api key from https://steamsets.com/settings/developer-apps
  20. const STEAMSETS_API_URL = 'https://api.steamsets.com/v1/app.listBadges';
  21. const STEAM_BADGE_INFO_URL = 'https://steamcommunity.com/my/ajaxgetbadgeinfo/';
  22. const STEAMSETS_API_CALL_DELAY_MS = 1000; // 1 second delay before Steamsets API calls
  23. const STEAM_API_CALL_DELAY_MS = 200; // 200ms delay between Steam's ajaxgetbadgeinfo calls
  24. const CACHE_EXPIRATION_MS = 7 * 24 * 60 * 60 * 1000; // 7 days in milliseconds
  25. const SCRIPT_TOGGLE_KEY = 'steamBadgeEnhancerEnabled'; // Key for the main script toggle
  26. const REQUEUE_BUTTON_TOGGLE_KEY = 'steamBadgeRequeueButtonEnabled'; // Key for the re-queue button toggle
  27.  
  28. // IndexedDB Configuration
  29. const DB_NAME = 'SteamBadgeCacheDB';
  30. const DB_VERSION = 1; // Increment this if you change the database structure
  31. const STORE_NAME = 'badgeCache';
  32. // --- End Configuration ---
  33.  
  34. // Get the current state of the toggle settings
  35. let isScriptEnabled = GM_getValue(SCRIPT_TOGGLE_KEY, true); // Default script enabled to true
  36. let isRequeueButtonEnabled = GM_getValue(REQUEUE_BUTTON_TOGGLE_KEY, true); // Default re-queue button enabled to true
  37. // Variable to hold the IndexedDB database instance
  38. let db = null;
  39.  
  40. // --- IndexedDB Functions ---
  41.  
  42. function openDatabase() {
  43. return new Promise((resolve, reject) => {
  44. const request = indexedDB.open(DB_NAME, DB_VERSION);
  45.  
  46. request.onerror = (event) => {
  47. console.error("IndexedDB database error:", event.target.error);
  48. reject(event.target.error);
  49. };
  50.  
  51. request.onupgradeneeded = (event) => {
  52. const db = event.target.result;
  53. // Create the object store if it doesn't exist
  54. if (!db.objectStoreNames.contains(STORE_NAME)) {
  55. db.createObjectStore(STORE_NAME, { keyPath: 'appId' });
  56. console.log(`IndexedDB object store "${STORE_NAME}" created.`);
  57. }
  58. // Future versions could add indexes here if needed for querying
  59. };
  60.  
  61. request.onsuccess = (event) => {
  62. db = event.target.result;
  63. console.log("IndexedDB database opened successfully.");
  64. resolve(db);
  65. };
  66. });
  67. }
  68.  
  69. async function getCacheEntry(appId) {
  70. if (!db) {
  71. console.warn("IndexedDB not initialized. Cannot get cache entry.");
  72. return null;
  73. }
  74.  
  75. return new Promise((resolve, reject) => {
  76. const transaction = db.transaction([STORE_NAME], 'readonly');
  77. const store = transaction.objectStore(STORE_NAME);
  78. const request = store.get(appId);
  79.  
  80. request.onerror = (event) => {
  81. console.error(`Error getting cache entry for appId ${appId} from IndexedDB:`, event.target.error);
  82. resolve(null); // Resolve with null on error
  83. };
  84.  
  85. request.onsuccess = (event) => {
  86. const cachedEntry = event.target.result;
  87. // console.log(`IndexedDB get success for appId ${appId}:`, cachedEntry); // Too chatty
  88. resolve(cachedEntry);
  89. };
  90. });
  91. }
  92.  
  93. async function setCacheEntry(appId, cacheEntry) {
  94. if (!db) {
  95. console.warn("IndexedDB not initialized. Cannot set cache entry.");
  96. return false;
  97. }
  98.  
  99. return new Promise((resolve, reject) => {
  100. const transaction = db.transaction([STORE_NAME], 'readwrite');
  101. const store = transaction.objectStore(STORE_NAME);
  102.  
  103. // Add the appId to the object since it's the keyPath
  104. cacheEntry.appId = appId;
  105.  
  106. const request = store.put(cacheEntry); // put() adds or updates
  107.  
  108. request.onerror = (event) => {
  109. console.error(`Error setting cache entry for appId ${appId} in IndexedDB:`, event.target.error);
  110. resolve(false); // Resolve with false on error
  111. };
  112.  
  113. request.onsuccess = (event) => {
  114. // console.log(`IndexedDB set success for appId ${appId}.`); // Too chatty
  115. resolve(true); // Resolve with true on success
  116. };
  117.  
  118. transaction.oncomplete = () => {
  119. // console.log(`IndexedDB transaction complete for appId ${appId}.`); // Too chatty
  120. // The resolve is already called in onsuccess
  121. };
  122. });
  123. }
  124.  
  125. async function removeCacheEntry(appId) {
  126. if (!db) {
  127. console.warn("IndexedDB not initialized. Cannot remove cache entry.");
  128. return false;
  129. }
  130.  
  131. return new Promise((resolve, reject) => {
  132. const transaction = db.transaction([STORE_NAME], 'readwrite');
  133. const store = transaction.objectStore(STORE_NAME);
  134. const request = store.delete(appId);
  135.  
  136. request.onerror = (event) => {
  137. console.error(`Error removing cache entry for appId ${appId} from IndexedDB:`, event.target.error);
  138. resolve(false); // Resolve with false on error
  139. };
  140.  
  141. request.onsuccess = (event) => {
  142. console.log(`Removed cache entry for App ID: ${appId} from IndexedDB.`);
  143. resolve(true); // Resolve with true on success
  144. };
  145.  
  146. transaction.oncomplete = () => {
  147. // console.log(`IndexedDB delete transaction complete for appId ${appId}.`); // Too chatty
  148. // The resolve is already called in onsuccess
  149. };
  150. });
  151. }
  152.  
  153. // --- End IndexedDB Functions ---
  154.  
  155.  
  156. // Add the toggle buttons
  157. function addToggleButtons() {
  158. const profileHeader = document.querySelector('.profile_header_actions, .profile_header_actions_secondary');
  159. if (!profileHeader) {
  160. console.warn("Could not find profile header actions to add toggle buttons.");
  161. return;
  162. }
  163.  
  164. // Add Main Script Toggle Button
  165. const scriptToggleButton = document.createElement('div');
  166. scriptToggleButton.id = 'steam-badge-enhancer-toggle';
  167. scriptToggleButton.style.cssText = `
  168. display: inline-block;
  169. margin-left: 10px;
  170. padding: 5px 10px;
  171. background-color: ${isScriptEnabled ? '#5cb85c' : '#d9534f'}; /* Green for enabled, Red for disabled */
  172. color: white;
  173. border-radius: 3px;
  174. cursor: pointer;
  175. font-size: 12px;
  176. line-height: 1.2;
  177. user-select: none;
  178. margin-bottom: 5px; /* Space between buttons */
  179. `;
  180. scriptToggleButton.textContent = `Enhancer: ${isScriptEnabled ? 'Enabled' : 'Disabled'}`;
  181. scriptToggleButton.title = `Click to ${isScriptEnabled ? 'disable' : 'enable'} the Steam Badge Enhancer script.`;
  182.  
  183. scriptToggleButton.addEventListener('click', () => {
  184. isScriptEnabled = !isScriptEnabled;
  185. GM_setValue(SCRIPT_TOGGLE_KEY, isScriptEnabled);
  186. scriptToggleButton.style.backgroundColor = isScriptEnabled ? '#5cb85c' : '#d9534f';
  187. scriptToggleButton.textContent = `Enhancer: ${isScriptEnabled ? 'Enabled' : 'Disabled'}`;
  188. scriptToggleButton.title = `Click to ${isScriptEnabled ? 'disable' : 'enable'} the Steam Badge Enhancer script.`;
  189. console.log(`Steam Badge Enhancer ${isScriptEnabled ? 'enabled' : 'disabled'}. Reload the page for changes to take full effect.`);
  190. // Reloading is recommended for the main toggle
  191. });
  192.  
  193. profileHeader.appendChild(scriptToggleButton);
  194.  
  195. // Add Re-queue Button Toggle Button
  196. const requeueToggle = document.createElement('div');
  197. requeueToggle.id = 'steam-badge-requeue-toggle';
  198. requeueToggle.style.cssText = `
  199. display: inline-block;
  200. margin-left: 10px;
  201. padding: 5px 10px;
  202. background-color: ${isRequeueButtonEnabled ? '#5cb85c' : '#d9534f'}; /* Green for enabled, Red for disabled */
  203. color: white;
  204. border-radius: 3px;
  205. cursor: pointer;
  206. font-size: 12px;
  207. line-height: 1.2;
  208. user-select: none;
  209. /* Position relative to allow margin-left */
  210. position: relative;
  211. top: 0px; /* Align with the first button if needed */
  212. `;
  213. requeueToggle.textContent = `Re-queue Button: ${isRequeueButtonEnabled ? 'Shown' : 'Hidden'}`;
  214. requeueToggle.title = `Click to ${isRequeueButtonEnabled ? 'hide' : 'show'} the Re-queue Data Fetch buttons.`;
  215.  
  216. requeueToggle.addEventListener('click', () => {
  217. isRequeueButtonEnabled = !isRequeueButtonEnabled;
  218. GM_setValue(REQUEUE_BUTTON_TOGGLE_KEY, isRequeueButtonEnabled);
  219. requeueToggle.style.backgroundColor = isRequeueButtonEnabled ? '#5cb85c' : '#d9534f';
  220. requeueToggle.textContent = `Re-queue Button: ${isRequeueButtonEnabled ? 'Shown' : 'Hidden'}`;
  221. requeueToggle.title = `Click to ${isRequeueButtonEnabled ? 'hide' : 'show'} the Re-queue Data Fetch buttons.`;
  222. console.log(`Re-queue buttons ${isRequeueButtonEnabled ? 'shown' : 'hidden'}.`);
  223.  
  224. // Immediately hide/show buttons without requiring a reload
  225. const buttons = document.querySelectorAll('.requeue_button');
  226. buttons.forEach(button => {
  227. button.style.display = isRequeueButtonEnabled ? '' : 'none';
  228. });
  229. });
  230.  
  231. // Find the parent of the first button and insert the second button after it
  232. if (scriptToggleButton.parentNode) {
  233. scriptToggleButton.parentNode.insertBefore(requeueToggle, scriptToggleButton.nextSibling);
  234. } else {
  235. // Fallback if parent not found (less likely)
  236. profileHeader.appendChild(requeueToggle);
  237. }
  238.  
  239.  
  240. console.log("Steam Badge Enhancer toggle buttons added.");
  241. }
  242.  
  243.  
  244. // Add script styles
  245. GM_addStyle(`
  246. /* Style for the container holding the appended badge details */
  247. .enhanced_badge_details_container {
  248. display: flex;
  249. flex-wrap: wrap;
  250. justify-content: center; /* Center the individual badges */
  251. align-items: flex-start; /* Align items to the top */
  252. margin-top: 10px; /* Space between original content and new content */
  253. padding-top: 10px;
  254. border-top: 1px solid #303030; /* Separator line */
  255. width: 100%; /* Take full width of the badge row */
  256. min-height: 80px; /* Ensure container has some height even when empty or loading */
  257. position: relative; /* Needed for absolute positioned loading indicators */
  258. }
  259.  
  260. .badge_info_container {
  261. display: flex;
  262. flex-direction: column;
  263. align-items: center;
  264. margin: 5px 15px; /* Increased horizontal margin (left and right) */
  265. text-align: center;
  266. width: 120px; /* Increased width of each badge container */
  267. flex-shrink: 0; /* Prevent shrinking */
  268. position: relative; /* Needed for highlight pseudo-element */
  269. }
  270.  
  271. .badge_image {
  272. width: 64px; /* Adjust image size as needed */
  273. height: 64px; /* Adjust image size as needed */
  274. object-fit: contain;
  275. margin-bottom: 5px;
  276. }
  277.  
  278. .badge_name {
  279. font-weight: bold;
  280. margin-bottom: 2px;
  281. font-size: 0.8em; /* Adjust font size for name */
  282. /* Added text truncation styles */
  283. white-space: nowrap;
  284. overflow: hidden;
  285. text-overflow: ellipsis;
  286. max-width: 100%; /* Ensure it respects the container width */
  287. }
  288.  
  289. .badge_xp, .badge_scarcity, .badge_completion_date, .badge_level_display { /* Added .badge_level_display */
  290. font-size: 0.8em; /* Adjust font size for these elements */
  291. color: #8f98a0; /* Adjust secondary text color */
  292. }
  293.  
  294. .badge_completion_date {
  295. font-size: 0.75em; /* Slightly smaller font for the date */
  296. white-space: nowrap; /* Prevent wrapping for the date */
  297. overflow: hidden; /* Hide overflow */
  298. text-overflow: ellipsis; /* Show ellipsis if still overflows */
  299. max-width: 100%; /* Ensure it respects the container width */
  300. }
  301.  
  302. /* Style for highlighting the crafted badge */
  303. .badge_info_container.crafted {
  304. box-shadow: 0 0 8px 2px rgb(154 155 255 / 50%);
  305. border: 1px solid #8e8e95;
  306. padding: 0px 5px 15px 5px;
  307. }
  308.  
  309. /* Style for the re-queue button */
  310. .requeue_button {
  311. padding: 3px 8px;
  312. max-width: 15px;
  313. background-color: #22558f;
  314. color: #ffffff;
  315. border: 1px solid #505050;
  316. border-radius: 3px;
  317. cursor: pointer;
  318. font-size: 1.3em;
  319. text-align: center;
  320. /* IMPORTANT: Stop click event propagation */
  321. z-index: 99999; /* Increased z-index */
  322. position: relative; /* Needed for z-index to work */
  323. }
  324.  
  325. .requeue_button:hover {
  326. background-color: #404040;
  327. }
  328.  
  329. /* Style for the initial loading/cached indicator */
  330. .initial_indicator {
  331. color: #8f98a0;
  332. font-style: italic;
  333. margin: 10px;
  334. width: 100%;
  335. display: block;
  336. text-align: center;
  337. }
  338.  
  339. /* Style for the loading overlay */
  340. .enhancer_loading_overlay {
  341. position: absolute;
  342. top: 0;
  343. left: 0;
  344. right: 0;
  345. bottom: 0;
  346. background-color: rgba(0, 0, 0, 0.7); /* Semi-transparent dark overlay */
  347. display: flex;
  348. justify-content: center;
  349. align-items: center;
  350. color: white;
  351. font-size: 1.2em;
  352. z-index: -10; /* Above badge details but below the container */
  353. }
  354.  
  355. `);
  356.  
  357. function extractAppIdFromBadgeLink(badgeRow) {
  358. const badgeLink = badgeRow.querySelector('a.badge_row_overlay');
  359. if (badgeLink) {
  360. const match = badgeLink.href.match(/\/gamecards\/(\d+)\//);
  361. if (match && match[1]) {
  362. return parseInt(match[1], 10);
  363. }
  364. }
  365.  
  366. // Fallback if primary link not found or doesn't have appid
  367. const badgeImageLink = badgeRow.querySelector('.badge_info_image a'); // Link around the badge image
  368. if (badgeImageLink) {
  369. const steamStoreLinkMatch = badgeImageLink.href.match(/\/app\/(\d+)\//);
  370. if (steamStoreLinkMatch && steamStoreLinkMatch[1]) {
  371. return parseInt(steamStoreLinkMatch[1], 10);
  372. }
  373. }
  374.  
  375. return null;
  376. }
  377.  
  378. async function getBadgeData(appId) {
  379. return new Promise((resolve, reject) => {
  380. GM_xmlhttpRequest({
  381. method: 'POST',
  382. url: STEAMSETS_API_URL,
  383. headers: {
  384. 'Content-Type': 'application/json',
  385. 'Authorization': `Bearer ${STEAMSETS_API_KEY}` // Use the single API key
  386. },
  387. data: JSON.stringify({ appId: appId }),
  388. onload: function(response) {
  389. try {
  390. const data = JSON.parse(response.responseText);
  391.  
  392. if (data && Array.isArray(data.badges)) {
  393. console.log(`Successfully fetched Steamsets badge data for appId ${appId}.`);
  394. resolve(data.badges);
  395. } else {
  396. console.error(`Steamsets API response for appId ${appId} did not contain a valid 'badges' array. Response data:`, data);
  397. resolve([]); // Resolve with empty array if data is not as expected
  398. }
  399. } catch (e) {
  400. console.error(`Error parsing Steamsets API response for appId ${appId}:`, e);
  401. resolve([]); // Resolve with empty array on parsing error
  402. }
  403. },
  404. onerror: function(error) {
  405. console.error(`GM_xmlhttpRequest error for appId ${appId}:`, error);
  406. resolve([]); // Resolve with empty array on request error
  407. }
  408. });
  409. });
  410. }
  411.  
  412. async function getCraftedBadgeInfo(appId, isFoil = false) {
  413. return new Promise((resolve, reject) => {
  414. let url = `${STEAM_BADGE_INFO_URL}${appId}`;
  415. if (isFoil) {
  416. url += '?border=1'; // Add parameter for foil badge info
  417. }
  418.  
  419. GM_xmlhttpRequest({
  420. method: 'GET',
  421. url: url,
  422. onload: function(response) {
  423. try {
  424. const data = JSON.parse(response.responseText); // Parse the JSON response
  425.  
  426. let craftedLevel = 0;
  427. let isCrafted = false;
  428.  
  429. // Check if badgedata and level exist in the JSON response
  430. if (data && data.badgedata && typeof data.badgedata.level === 'number') {
  431. craftedLevel = data.badgedata.level;
  432. // Consider it crafted if the level is greater than 0
  433. isCrafted = craftedLevel > 0;
  434. }
  435.  
  436. // console.log(`Fetched crafted info for appId ${appId} (Foil: ${isFoil}): Crafted Level = ${craftedLevel}, Is Crafted = ${isCrafted}`); // Too chatty
  437.  
  438. // Return the crafted level and whether a badge (at any level) is crafted
  439. resolve({ craftedLevel: craftedLevel, isCrafted: isCrafted });
  440.  
  441. } catch (e) {
  442. console.error(`Error parsing Steam badge info JSON for appId ${appId} (Foil: ${isFoil}):`, e);
  443. resolve({ craftedLevel: 0, isCrafted: false }); // Resolve with default values on error
  444. }
  445. },
  446. onerror: function(error) {
  447. console.error(`GM_xmlhttpRequest error fetching Steam badge info for appId ${appId} (Foil: ${isFoil}):`, error);
  448. resolve({ craftedLevel: 0, isCrafted: false }); // Resolve with default values on error
  449. }
  450. });
  451. });
  452. }
  453.  
  454.  
  455. // Helper function for introducing a delay
  456. function delay(ms) {
  457. return new Promise(resolve => setTimeout(resolve, ms));
  458. }
  459.  
  460. // Helper function to format date for display
  461. function formatDateForDisplay(dateString) {
  462. try {
  463. const date = new Date(dateString);
  464. if (isNaN(date.getTime())) {
  465. return 'Date unavailable';
  466. }
  467.  
  468. const options = {
  469. month: 'short', // 'Jan', 'Feb', etc.
  470. day: 'numeric',
  471. year: 'numeric',
  472. hour: 'numeric', // 12-hour format
  473. minute: 'numeric',
  474. hour12: true // Use 12-hour format with AM/PM
  475. };
  476.  
  477. // Use locale-specific formatting, then replace commas for the desired format
  478. let formatted = date.toLocaleString(undefined, options);
  479.  
  480. // The default toLocaleString with 'short' month and numeric day/year
  481. // often results in "Month Day, Year, HH:MM AM/PM".
  482. // We'll return this format directly.
  483.  
  484. return formatted;
  485.  
  486. } catch (e) {
  487. console.error("Error formatting date:", e);
  488. return 'Date Formatting Error';
  489. }
  490. }
  491.  
  492.  
  493. // --- IndexedDB Caching Functions (Replacing Local Storage) ---
  494.  
  495. function isCacheValid(cachedEntry) {
  496. if (!cachedEntry) {
  497. return false; // Entry doesn't exist
  498. }
  499. // Check for basic structure required
  500. // Note: The appId is now part of the stored object due to keyPath
  501. if (typeof cachedEntry.timestamp !== 'number' ||
  502. !cachedEntry.steamsetsData ||
  503. typeof cachedEntry.craftedNormalInfo !== 'object' ||
  504. typeof cachedEntry.craftedFoilInfo !== 'object' ||
  505. typeof cachedEntry.appId !== 'number') { // Check for appId as well
  506. console.log("Cache entry has invalid structure:", cachedEntry);
  507. return false; // Invalid structure
  508. }
  509.  
  510. const now = Date.now();
  511. const isValid = (now - cachedEntry.timestamp) < CACHE_EXPIRATION_MS;
  512. return isValid;
  513. }
  514.  
  515.  
  516. // Map to store badge rows grouped by App ID (needed globally for re-queue)
  517. const badgeRowsByAppId = new Map();
  518. // Queue for App IDs to process (fetch new data)
  519. const fetchQueue = [];
  520. // Flag to prevent multiple fetch loops running simultaneously
  521. let isFetching = false;
  522. // Set to track App IDs currently being fetched
  523. const currentlyFetching = new Set();
  524.  
  525.  
  526. // Function to add an App ID to the fetch queue
  527. function queueAppIdForFetch(appId) {
  528. // Only add if not already in the queue or being processed
  529. if (!fetchQueue.includes(appId) && !currentlyFetching.has(appId)) {
  530. fetchQueue.push(appId);
  531. console.log(`App ID ${appId} added to fetch queue. Queue size: ${fetchQueue.length}`);
  532. // If not already fetching, start the fetch loop
  533. if (!isFetching) {
  534. processFetchQueue();
  535. }
  536. } else {
  537. // console.log(`App ID ${appId} is already in the fetch queue or being fetched.`); // Too chatty
  538. }
  539. }
  540.  
  541. // Main function to process the fetch queue
  542. async function processFetchQueue() {
  543. if (isFetching || fetchQueue.length === 0) {
  544. return; // Don't start if already fetching or queue is empty
  545. }
  546.  
  547. isFetching = true;
  548. console.log("Starting fetch queue processing.");
  549.  
  550. while (fetchQueue.length > 0) {
  551. const appId = fetchQueue[0]; // Peek at the next App ID
  552.  
  553. // Ensure the App ID is added to the currentlyFetching set before processing
  554. currentlyFetching.add(appId);
  555.  
  556. const rowsForApp = badgeRowsByAppId.get(appId) || [];
  557.  
  558. if (rowsForApp.length === 0) {
  559. console.log(`No badge rows found for App ID ${appId} in badgeRowsByAppId map for fetch. Removing from queue.`);
  560. fetchQueue.shift(); // Remove the item if no rows found
  561. currentlyFetching.delete(appId); // Remove from fetching set
  562. continue; // Skip
  563. }
  564.  
  565. console.log(`Processing App ID ${appId} from fetch queue.`);
  566.  
  567. // Add loading indicator overlay to each row
  568. rowsForApp.forEach(badgeRow => {
  569. let enhancedBadgeDetailsContainer = badgeRow.querySelector('.enhanced_badge_details_container');
  570. if (!enhancedBadgeDetailsContainer) {
  571. console.error(`Container not found for App ID ${appId} during fetch process.`);
  572. return; // Skip this row if container is missing (shouldn't happen after initialSetup)
  573. }
  574.  
  575. // Check if an overlay already exists
  576. let loadingOverlay = enhancedBadgeDetailsContainer.querySelector('.enhancer_loading_overlay');
  577. if (!loadingOverlay) {
  578. loadingOverlay = document.createElement('div');
  579. loadingOverlay.classList.add('enhancer_loading_overlay');
  580. loadingOverlay.textContent = "Updating data...";
  581. enhancedBadgeDetailsContainer.appendChild(loadingOverlay);
  582. } else {
  583. loadingOverlay.textContent = "Updating data..."; // Update text if it exists
  584. loadingOverlay.style.display = 'flex'; // Ensure it's visible
  585. }
  586. });
  587.  
  588.  
  589. // --- Fetch Steamsets data with 1-second delay ---
  590. await delay(STEAMSETS_API_CALL_DELAY_MS);
  591. const badgeData = await getBadgeData(appId);
  592.  
  593. // --- Fetch Crafted Badge Info (Normal) with shorter delay ---
  594. await delay(STEAM_API_CALL_DELAY_MS);
  595. const craftedNormalInfo = await getCraftedBadgeInfo(appId, false);
  596.  
  597. // --- Fetch Crafted Badge Info (Foil) with shorter delay ---
  598. await delay(STEAM_API_CALL_DELAY_MS);
  599. const craftedFoilInfo = await getCraftedBadgeInfo(appId, true);
  600.  
  601. // Prepare cache entry
  602. const cacheEntry = {
  603. timestamp: Date.now(),
  604. steamsetsData: badgeData,
  605. craftedNormalInfo: craftedNormalInfo,
  606. craftedFoilInfo: craftedFoilInfo
  607. };
  608.  
  609. // Store fetched data in IndexedDB
  610. await setCacheEntry(appId, cacheEntry); // Use the new IndexedDB set function
  611.  
  612. // Display the updated data *before* removing the overlay
  613. displayBadgeDetails(appId, rowsForApp, badgeData, craftedNormalInfo, craftedFoilInfo, false); // Displaying fresh data
  614.  
  615. // Remove the loading overlay after displaying new data
  616. rowsForApp.forEach(badgeRow => {
  617. const enhancedBadgeDetailsContainer = badgeRow.querySelector('.enhanced_badge_details_container');
  618. const loadingOverlay = enhancedBadgeDetailsContainer ? enhancedBadgeDetailsContainer.querySelector('.enhancer_loading_overlay') : null;
  619. if (loadingOverlay) {
  620. loadingOverlay.remove(); // Remove the overlay element
  621. }
  622. });
  623.  
  624.  
  625. fetchQueue.shift(); // Remove the item from the queue AFTER successful fetch and cache
  626. currentlyFetching.delete(appId); // Remove from fetching set
  627.  
  628. }
  629.  
  630. isFetching = false;
  631. console.log("Finished fetch queue processing.");
  632. }
  633.  
  634.  
  635. // Function to display badge details (extracted for reusability)
  636. function displayBadgeDetails(appId, rowsForApp, badgeData, craftedNormalInfo, craftedFoilInfo, isCached) {
  637. console.log(`Displaying details for App ID ${appId}. Data is cached: ${isCached}`);
  638.  
  639. // Find the existing container (created in initialSetup or processFetchQueue)
  640. rowsForApp.forEach(badgeRow => {
  641. const container = badgeRow.querySelector('.enhanced_badge_details_container');
  642. if(container) {
  643. // Keep the container, but replace its *content* (excluding the overlay if it exists)
  644. const loadingOverlay = container.querySelector('.enhancer_loading_overlay'); // Find existing overlay
  645.  
  646. // Create a temporary container to hold the new badge details HTML
  647. const tempDiv = document.createElement('div');
  648.  
  649. if (badgeData.length > 0) {
  650. // Sort badges: Levels 1-5 (non-foil) then Foil
  651. const sortedBadges = badgeData.sort((a, b) => {
  652. if (a.isFoil === b.isFoil) {
  653. return a.baseLevel - b.baseLevel; // Sort by level if same foil status
  654. }
  655. return a.isFoil ? 1 : -1; // Foil comes after non-foil
  656. });
  657.  
  658. // Create the HTML content for the detailed badges
  659. const detailedBadgesHtml = sortedBadges.map(badge => {
  660. const formattedCompletionDate = badge.firstCompletion ? formatDateForDisplay(badge.firstCompletion) : 'Date unavailable';
  661.  
  662. // Determine if this badge level should be highlighted
  663. let isCraftedHighlight = false;
  664. if (!badge.isFoil && craftedNormalInfo.isCrafted && craftedNormalInfo.craftedLevel === badge.baseLevel) {
  665. isCraftedHighlight = true;
  666. } else if (badge.isFoil && craftedFoilInfo.isCrafted && craftedFoilInfo.craftedLevel === badge.baseLevel) {
  667. isCraftedHighlight = true;
  668. }
  669.  
  670. // Add 'crafted' class if it needs highlighting
  671. const containerClass = isCraftedHighlight ? 'badge_info_container crafted' : 'badge_info_container';
  672.  
  673. return `
  674. <div class="${containerClass}">
  675. <div class="badge_name" title="${badge.name}">${badge.name}</div>
  676. <img class="badge_image" src="https://cdn.fastly.steamstatic.com/steamcommunity/public/images/items/${appId}/${badge.badgeImage}" alt="${badge.name}">
  677. <div class="badge_completion_date">${formattedCompletionDate}</div> <!-- Added First Completion Date -->
  678. <div class="badge_scarcity">Scarcity: ${badge.scarcity}</div>
  679. <div class="badge_level_display">Level: ${badge.baseLevel}${badge.isFoil ? ' (Foil)' : ''}</div> <!-- Added Level Display -->
  680. </div>
  681. `;
  682. }).join(''); // Join the array of HTML strings into a single string
  683.  
  684. tempDiv.innerHTML = detailedBadgesHtml;
  685.  
  686. } else {
  687. console.log(`No detailed badge data found for appId ${appId}. Displaying "No data available".`);
  688. // Display a message
  689. const noDataMessage = document.createElement('div');
  690. noDataMessage.textContent = `No detailed badge data available for this game (App ID: ${appId}).`;
  691. noDataMessage.style.color = '#8f98a0';
  692. noDataMessage.style.margin = '10px auto'; // Center the message
  693. tempDiv.appendChild(noDataMessage);
  694. }
  695.  
  696. // Add a visual indicator if data was from cache
  697. if (isCached) {
  698. const cachedIndicator = document.createElement('div');
  699. cachedIndicator.textContent = `⠀`; // Text indicating cache
  700. cachedIndicator.style.fontSize = '0.7em';
  701. cachedIndicator.style.color = '#8f98a0';
  702. cachedIndicator.style.textAlign = 'center';
  703. cachedIndicator.style.marginTop = '5px';
  704. // Prepend to the temporary container
  705. tempDiv.insertBefore(cachedIndicator, tempDiv.firstChild);
  706. }
  707.  
  708. // Replace the *content* of the main container with the content from tempDiv
  709. // This avoids removing and re-adding the container itself or the overlay
  710. while(container.firstChild && container.firstChild !== loadingOverlay) {
  711. container.removeChild(container.firstChild);
  712. }
  713. while(tempDiv.firstChild) {
  714. container.insertBefore(tempDiv.firstChild, loadingOverlay); // Insert before the overlay if it exists
  715. }
  716.  
  717.  
  718. // Add the re-queue button if enabled, ENSURING IT DOESN'T ALREADY EXIST
  719. // This part remains the same as it's added to badge_title_stats, not the enhanced_badge_details_container
  720. if (isRequeueButtonEnabled) {
  721. const badgeTitleStatsContainer = badgeRow.querySelector('.badge_title_stats');
  722. // Check if a re-queue button already exists within this container
  723. const existingRequeueButton = badgeTitleStatsContainer ? badgeTitleStatsContainer.querySelector('.requeue_button') : null;
  724.  
  725. if (badgeTitleStatsContainer && !existingRequeueButton) { // Only add if container exists and button doesn't exist
  726. const requeueButton = document.createElement('div');
  727. requeueButton.classList.add('requeue_button');
  728. requeueButton.textContent = ' ↻​ ';
  729. // Store the appId on the button for easy access in the event listener
  730. requeueButton.dataset.appId = appId;
  731. badgeTitleStatsContainer.appendChild(requeueButton);
  732.  
  733. // Add click listener to the button
  734. requeueButton.addEventListener('click', handleRequeueClick);
  735. } else if (!badgeTitleStatsContainer) {
  736. console.warn(`Could not find .badge_title_stats container for App ID ${appId} to add re-queue button.`);
  737. }
  738. }
  739.  
  740.  
  741. } else {
  742. console.error(`Could not find .enhanced_badge_details_container for App ID ${appId} during display.`);
  743. }
  744. });
  745. }
  746.  
  747.  
  748. // Event handler for the re-queue button
  749. function handleRequeueClick(event) {
  750. event.preventDefault(); // Prevent default link behavior
  751. event.stopPropagation(); // *** IMPORTANT: Stop click event from bubbling up ***
  752.  
  753. const appId = parseInt(event.target.dataset.appId, 10);
  754. if (!isNaN(appId)) {
  755. console.log(`Manual re-queue requested for App ID: ${appId}`);
  756. removeCacheEntry(appId); // Remove the old cache entry
  757. queueAppIdForFetch(appId); // Add to the fetch queue
  758. } else {
  759. console.error("Could not get App ID from re-queue button data.");
  760. }
  761. }
  762.  
  763. // Function to delete elements with class 'badge_title_stats_drops' and 'badge_title_stats_playtime'
  764. function deleteBadgeStats() {
  765. const dropsElements = document.querySelectorAll('.badge_title_stats_drops');
  766. dropsElements.forEach(element => {
  767. element.remove();
  768. // console.log("Removed element with class 'badge_title_stats_drops'"); // Too chatty
  769. });
  770. const playtimeElements = document.querySelectorAll('.badge_title_stats_playtime');
  771. playtimeElements.forEach(element => {
  772. element.remove();
  773. // console.log("Removed element with class 'badge_title_stats_playtime'"); // Too chatty
  774. });
  775. }
  776.  
  777.  
  778. // Initial setup: Collect all badge rows, display cached, and queue uncached/expired
  779. async function initialSetup() { // Make initialSetup async because it awaits openDatabase and getCacheEntry
  780.  
  781. // Add the toggle buttons first
  782. addToggleButtons();
  783.  
  784. // Check if the script is enabled AFTER adding toggle buttons
  785. if (!isScriptEnabled) {
  786. console.log("Steam Badge Enhancer is disabled. Toggle buttons are available.");
  787. // Still delete the unnecessary stats elements even if enhancement is disabled
  788. deleteBadgeStats();
  789. return; // Exit the function if disabled
  790. }
  791.  
  792. console.log("Steam Badge Enhancer: Initial setup.");
  793.  
  794. // --- Initialize IndexedDB ---
  795. try {
  796. await openDatabase();
  797. } catch (error) {
  798. console.error("Failed to open IndexedDB. Caching will not be available.", error);
  799. // Decide how to proceed if IndexedDB fails. For now, we'll continue
  800. // but caching functions will log warnings.
  801. }
  802. // --- End IndexedDB Initialization ---
  803.  
  804. // Delete the unnecessary stats elements immediately
  805. deleteBadgeStats();
  806.  
  807.  
  808. const allBadgeRows = document.querySelectorAll('.badge_row.is_link');
  809.  
  810. // Group badge rows by App ID
  811. allBadgeRows.forEach(badgeRow => {
  812. const appId = extractAppIdFromBadgeLink(badgeRow);
  813. if (appId) {
  814. if (!badgeRowsByAppId.has(appId)) {
  815. badgeRowsByAppId.set(appId, []);
  816. }
  817. badgeRowsByAppId.get(appId).push(badgeRow);
  818. } else {
  819. console.warn("Could not extract App ID from a badge row:", badgeRow);
  820. }
  821. });
  822.  
  823. console.log(`Initial setup found ${badgeRowsByAppId.size} unique App IDs.`);
  824.  
  825. const appIdsOnPage = new Set(badgeRowsByAppId.keys()); // Get all App IDs found on the page
  826.  
  827. // Process App IDs on the page
  828. for (const appId of appIdsOnPage) { // Use for...of loop to allow await inside
  829. const rowsForApp = badgeRowsByAppId.get(appId);
  830. if (!rowsForApp || rowsForApp.length === 0) {
  831. console.warn(`No rows found for App ID ${appId} during initial setup.`);
  832. continue; // Skip if no rows found
  833. }
  834.  
  835. // Add the initial container to each row immediately
  836. rowsForApp.forEach(badgeRow => {
  837. const enhancedBadgeDetailsContainer = document.createElement('div');
  838. enhancedBadgeDetailsContainer.classList.add('enhanced_badge_details_container');
  839. // Add an initial indicator message
  840. const initialIndicator = document.createElement('div');
  841. initialIndicator.classList.add('initial_indicator');
  842. initialIndicator.textContent = "Checking cache..."; // Initial state
  843. enhancedBadgeDetailsContainer.appendChild(initialIndicator);
  844. badgeRow.appendChild(enhancedBadgeDetailsContainer);
  845. });
  846.  
  847.  
  848. // Try to get cache entry from IndexedDB (async)
  849. const cachedEntry = await getCacheEntry(appId); // Await the IndexedDB get
  850.  
  851. if (isCacheValid(cachedEntry)) {
  852. console.log(`Found valid cache for App ID: ${appId}. Displaying immediately.`);
  853. // Immediately display cached data
  854. displayBadgeDetails(appId, rowsForApp, cachedEntry.steamsetsData, cachedEntry.craftedNormalInfo, cachedEntry.craftedFoilInfo, true);
  855. // Queue for background refresh if cache is old but still valid
  856. const now = Date.now();
  857. if ((now - cachedEntry.timestamp) > (CACHE_EXPIRATION_MS / 2)) { // Example: refresh if cache is older than half the expiration time
  858. console.log(`Cache for App ID ${appId} is older than half the expiration, queueing for background refresh.`);
  859. queueAppIdForFetch(appId);
  860. }
  861.  
  862.  
  863. } else {
  864. console.log(`No valid cache for App ID: ${appId}. Queueing for fetch.`);
  865. // Update indicator to reflect API loading
  866. rowsForApp.forEach(badgeRow => {
  867. const indicator = badgeRow.querySelector('.enhanced_badge_details_container .initial_indicator');
  868. if(indicator) indicator.textContent = "Loading detailed badge data...";
  869. });
  870. // Queue for background fetching
  871. queueAppIdForFetch(appId);
  872. }
  873. }
  874.  
  875. // The processFetchQueue function will start automatically if the fetchQueue is not empty
  876. }
  877.  
  878.  
  879. // Run the initial setup when the page is loaded
  880. window.addEventListener('load', initialSetup);
  881.  
  882. })();

QingJ © 2025

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