您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Injects badge information into the booster creator page using a user-defined ID. Includes a favorites list using IndexedDB and Steam-styled controls. Caches SteamSets API data using IndexedDB with a 1-week timeout. Adds import/export for favorites. Displays badge unlock date if available.
// ==UserScript== // @name Steam Badge Info for Booster Creator // @namespace https://github.com/encumber/ // @version 1.29 // @description Injects badge information into the booster creator page using a user-defined ID. Includes a favorites list using IndexedDB and Steam-styled controls. Caches SteamSets API data using IndexedDB with a 1-week timeout. Adds import/export for favorites. Displays badge unlock date if available. // @author Nitoned // @match https://steamcommunity.com/tradingcards/boostercreator/* // @match https://steamcommunity.com//tradingcards/boostercreator/* // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_openInTab // @grant GM_notification // @grant indexedDB // ==/UserScript== // ==================================================================== // === USER CONFIGURATION - EDIT THESE VALUES BEFORE SAVING SCRIPT === // ==================================================================== // IMPORTANT: Replace the placeholder value below with your actual Steam ID. // This can be your custom URL name (e.g., 'myprofile') or your SteamID64 (e.g., '76561198012345678'). const USER_STEAM_ID = "client"; // <--- PUT YOUR STEAM ID HERE // steamsets api key for listing all badge icons // If you are getting 401 errors, double-check your key on the SteamSets website. // It's possible the key expired, was revoked, or you copied it correctly. const SteamSetsAPI = ""; // <--- PUT YOUR SteamSets API KEY HERE // Set this to 'true' if the USER_STEAM_ID you entered is a SteamID64. // Set this to 'false' if the USER_STEAM_ID you entered is a custom URL name. const STEAM_ID_IS_STEAMID64 = false; // <--- SET TO true FOR STEAMID64, false FOR CUSTOM URL NAME // Set this to 'true' to enable detailed logging messages in your browser's console. // Set this to 'false' to disable all console logs from this script. const ENABLE_CONSOLE_LOGS = true; // <--- SET TO true OR false FOR LOGGING - KEEP TRUE FOR NOW // Default sort order for favorites ('appid_asc', 'appid_desc', 'foil_first', 'foil_last') // This is saved using GM_setValue/GM_getValue let currentFavoritesSortOrder = GM_getValue('favoritesSortOrder', 'appid_asc'); // Load saved sort order // Cache timeout for SteamSets API data in milliseconds (1 week) const API_CACHE_TIMEOUT_MS = 7 * 24 * 60 * 60 * 1000; // 7 days in milliseconds // ==================================================================== // === END USER CONFIGURATION - DO NOT EDIT BELOW THIS LINE UNLESS === // === YOU UNDERSTAND THE CODE === // ==================================================================== (function() { 'use strict'; // --- Logging Helper --- function log(...args) { if (ENABLE_CONSOLE_LOGS) { console.log('[Steam Badge Info]', ...args); } } function logError(...args) { if (ENABLE_CONSOLE_LOGS) { console.error('[Steam Badge Info]', ...args); } } function logWarn(...args) { if (ENABLE_CONSOLE_LOGS) { console.warn('[Steam Badge Info]', ...args); } } function logDebug(...args) { if (ENABLE_CONSOLE_LOGS) { console.debug('[Steam Badge Info Debug]', ...args); } } // --- End Logging Helper --- // --- Global State for Scroll Back --- let scrollPositionBeforeUpdate = 0; let clickedFavoriteAppId = null; // Store the app ID of the clicked favorite let clickedFavoriteIsFoil = null; // Store the foil status of the clicked favorite // --- End Global State --- // --- IndexedDB Setup --- const DB_NAME = 'SteamBadgeCache'; const API_CACHE_STORE_NAME = 'steamSetsBadges'; const FAVORITES_STORE_NAME = 'favorites'; // New store for favorites const DB_VERSION = 2; // Version 2 includes the 'favorites' store let db = null; function openDatabase() { return new Promise((resolve, reject) => { if (db) { logDebug('Database already open, resolving.'); resolve(db); return; } // Explicitly access indexedDB from the window object const browserIndexedDB = window.indexedDB || window.mozIndexedIndexedDB || window.webkitIndexedIndexedDB || window.msIndexedIndexedDB; if (!browserIndexedDB) { const error = new Error('IndexedDB is not supported in this environment.'); logError('IndexedDB not supported:', error); reject(error); return; } logDebug(`Opening IndexedDB: ${DB_NAME} (Version ${DB_VERSION})`); const request = browserIndexedDB.open(DB_NAME, DB_VERSION); request.onupgradeneeded = (event) => { const db = event.target.result; logDebug(`IndexedDB upgrade needed: oldVersion=${event.oldVersion}, newVersion=${event.newVersion}`); // Create object store for API cache if it doesn't exist if (!db.objectStoreNames.contains(API_CACHE_STORE_NAME)) { const apiStore = db.createObjectStore(API_CACHE_STORE_NAME, { keyPath: 'appId' }); logDebug(`IndexedDB upgrade: store "${API_CACHE_STORE_NAME}" created.`); } else { logDebug(`IndexedDB upgrade: store "${API_CACHE_STORE_NAME}" already exists.`); } // Create object store for favorites if it doesn't exist if (!db.objectStoreNames.contains(FAVORITES_STORE_NAME)) { // Favorites need a unique key, combination of appId and isFoil const favoritesStore = db.createObjectStore(FAVORITES_STORE_NAME, { keyPath: 'id' }); // We can add indexes for easier querying if needed later, e.g., by appId // favoritesStore.createIndex('appId', 'appId', { unique: false }); // favoritesStore.createIndex('isFoil', 'isFoil', { unique: false }); logDebug(`IndexedDB upgrade: store "${FAVORITES_STORE_NAME}" created.`); } else { logDebug(`IndexedDB upgrade: store "${FAVORITES_STORE_NAME}" already exists.`); } logDebug('IndexedDB upgrade finished.'); }; request.onsuccess = (event) => { db = event.target.result; logDebug('IndexedDB opened successfully.'); resolve(db); }; request.onerror = (event) => { logError('IndexedDB error:', event.target.error); reject(event.target.error); }; }); } // --- API Cache Functions --- async function getCachedData(appId) { try { logDebug(`Attempting to get cached data for App ID ${appId}.`); const database = await openDatabase(); // <-- Ensure this resolves logDebug(`Database opened for getting cached data for App ID ${appId}.`); const transaction = database.transaction(API_CACHE_STORE_NAME, 'readonly'); const store = transaction.objectStore(API_CACHE_STORE_NAME); const request = store.get(parseInt(appId)); return new Promise((resolve, reject) => { request.onsuccess = (event) => { const cachedItem = event.target.result; if (cachedItem) { const now = Date.now(); if (now - cachedItem.timestamp < API_CACHE_TIMEOUT_MS) { logDebug(`API Cache hit for App ID ${appId}. Data is fresh.`); resolve(cachedItem.data); } else { logDebug(`API Cache hit for App ID ${appId}, but data is stale. Will re-fetch.`); resolve(null); // Data is stale } } else { logDebug(`API Cache miss for App ID ${appId}.`); resolve(null); // No cached item found } }; request.onerror = (event) => { logError('Error getting data from API cache IndexedDB request:', event.target.error); // Crucially, reject the promise here so the catch block in fetchAndDisplayBadgeList is triggered reject(event.target.error); }; }); } catch (error) { logError('Error opening IndexedDB for API cache get:', error); // Crucially, re-throw or reject here so the catch block in fetchAndDisplayBadgeList is triggered throw error; // Re-throw the error } } async function cacheData(appId, data) { try { logDebug(`Attempting to cache data for App ID ${appId}.`); const database = await openDatabase(); // <-- Ensure this resolves logDebug(`Database opened for caching data for App ID ${appId}.`); const transaction = database.transaction(API_CACHE_STORE_NAME, 'readwrite'); const store = transaction.objectStore(API_CACHE_STORE_NAME); const itemToCache = { appId: parseInt(appId), timestamp: Date.now(), data: data }; const request = store.put(itemToCache); return new Promise((resolve, reject) => { request.onsuccess = () => { logDebug(`Data for App ID ${appId} cached successfully.`); resolve(); }; request.onerror = (event) => { logError('Error caching data in API cache IndexedDB request:', event.target.error); // Crucially, reject the promise here reject(event.target.error); }; // Add transaction completion handler for additional logging transaction.oncomplete = () => { logDebug(`API cache transaction for App ID ${appId} completed.`); }; transaction.onerror = (event) => { logError(`API cache transaction for App ID ${appId} failed:`, event.target.error); }; transaction.onabort = (event) => { logWarn(`API cache transaction for App ID ${appId} aborted:`, event.target.error); }; }); } catch (error) { logError('Error opening IndexedDB for API cache:', error); // Crucially, re-throw or reject here throw error; // Re-throw the error } } async function clearStaleApiCache() { try { const database = await openDatabase(); const transaction = database.transaction(API_CACHE_STORE_NAME, 'readwrite'); const store = transaction.objectStore(API_CACHE_STORE_NAME); const now = Date.now(); const request = store.openCursor(); request.onsuccess = (event) => { const cursor = event.target.result; if (cursor) { if (now - cursor.value.timestamp > API_CACHE_TIMEOUT_MS) { logDebug(`Deleting stale API cache for App ID ${cursor.value.appId}`); cursor.delete(); } cursor.continue(); } else { logDebug('Finished clearing stale API cache.'); } }; request.onerror = (event) => { logError('Error clearing stale API cache:', event.target.error); }; } catch (error) { logError('Error opening IndexedDB for clearing stale API cache:', error); } } // --- Favorites Functions (using IndexedDB) --- async function getFavorites() { logDebug('Attempting to get favorites from IndexedDB...'); try { const database = await openDatabase(); const transaction = database.transaction(FAVORITES_STORE_NAME, 'readonly'); const store = transaction.objectStore(FAVORITES_STORE_NAME); const request = store.getAll(); // Get all items return new Promise((resolve, reject) => { request.onsuccess = (event) => { const favorites = event.target.result || []; logDebug('Successfully fetched favorites from IndexedDB:', favorites); resolve(favorites); }; request.onerror = (event) => { logError('Error getting favorites from IndexedDB request:', event.target.error); reject(event.target.error); }; }); } catch (error) { logError('Error opening IndexedDB for getting favorites:', error); // Resolve with empty array if database opening fails return []; } } async function toggleFavorite(badgeData) { logDebug('Attempting to toggle favorite:', badgeData); const favoriteId = `${badgeData.appId}_${badgeData.isFoil ? 'foil' : 'regular'}`; logDebug(`Generated favorite ID: ${favoriteId}`); try { const database = await openDatabase(); const transaction = database.transaction(FAVORITES_STORE_NAME, 'readwrite'); const store = transaction.objectStore(FAVORITES_STORE_NAME); const getRequest = store.get(favoriteId); // Check if it exists getRequest.onsuccess = async (event) => { const existingFavorite = event.target.result; logDebug(`IndexedDB get result for ID ${favoriteId}:`, existingFavorite); if (existingFavorite) { // Item exists, remove it logDebug(`Favorite ${favoriteId} exists, attempting to delete.`); const deleteRequest = store.delete(favoriteId); deleteRequest.onsuccess = () => { log(`Removed favorite: App ID ${badgeData.appId}, Foil: ${badgeData.isFoil}`); displayFavorites(); // Refresh the displayed list }; deleteRequest.onerror = (event) => { logError('Error deleting favorite:', event.target.error); // Even on delete error, try to refresh display displayFavorites(); }; } else { // Item does not exist, add it logDebug(`Favorite ${favoriteId} does not exist, attempting to add.`); const newFavorite = { id: favoriteId, // Add the unique ID appId: badgeData.appId, name: badgeData.name || 'Unknown Badge', imageUrl: badgeData.imageUrl, isFoil: badgeData.isFoil // Should already be boolean from createBadgeListItem }; logDebug('New favorite data to add:', newFavorite); const putRequest = store.put(newFavorite); putRequest.onsuccess = () => { log(`Added favorite: App ID ${badgeData.appId}, Foil: ${badgeData.isFoil}`); displayFavorites(); // Refresh the displayed list }; putRequest.onerror = (event) => { logError('Error adding favorite:', event.target.error); // Even on add error, try to refresh display displayFavorites(); }; } }; getRequest.onerror = (event) => { logError('Error checking for existing favorite in IndexedDB:', event.target.error); // If checking fails, we can't proceed, but let's still try to display current state displayFavorites(); }; } catch (error) { logError('Error opening IndexedDB for toggling favorite:', error); // If opening fails, we can't proceed, but let's still try to display current state displayFavorites(); } } function sortFavorites(favorites, order) { switch (order) { case 'appid_asc': return favorites.sort((a, b) => parseInt(a.appId) - parseInt(b.appId)); case 'appid_desc': return favorites.sort((a, b) => parseInt(b.appId) - parseInt(a.appId)); case 'foil_first': // Changed sort order based on the dropdown text return favorites.sort((a, b) => { if (a.isFoil !== b.isFoil) { return a.isFoil ? -1 : 1; // true (foil) comes before false (non-foil) } return parseInt(a.appId) - parseInt(b.appId); // Secondary sort by appid }); case 'foil_last': // Changed sort order based on the dropdown text return favorites.sort((a, b) => { if (a.isFoil !== b.isFoil) { return a.isFoil ? 1 : -1; // false (non-foil) comes before true (foil) } return parseInt(a.appId) - parseInt(a.appId); // Secondary sort by appid }); default: return favorites; // Default to appid_asc if unknown order } } async function displayFavorites() { logDebug('Attempting to display favorites...'); const favoritesContainer = document.querySelector('.favorites-container'); if (!favoritesContainer) { logWarn('Favorites container not found. Cannot display favorites.'); return; } // Find or create the items wrapper let itemsWrapper = favoritesContainer.querySelector('.favorites-items-wrapper'); if (!itemsWrapper) { logDebug('Favorites items wrapper not found, creating.'); itemsWrapper = document.createElement('div'); itemsWrapper.className = 'favorites-items-wrapper'; favoritesContainer.appendChild(itemsWrapper); } itemsWrapper.innerHTML = ''; // Clear current items try { const favorites = await getFavorites(); // Get favorites asynchronously logDebug(`Fetched ${favorites.length} favorites for display.`); const sortedFavorites = sortFavorites([...favorites], currentFavoritesSortOrder); // Sort a copy if (sortedFavorites.length === 0) { itemsWrapper.textContent = 'No favorites added yet. Click a badge below to add it!'; itemsWrapper.style.color = '#B8B6B4'; itemsWrapper.style.textAlign = 'center'; itemsWrapper.style.width = '100%'; itemsWrapper.style.marginTop = '10px'; itemsWrapper.style.minHeight = '100px'; // Give it a minimum height even when empty } else { itemsWrapper.style.color = ''; // Reset styles itemsWrapper.style.textAlign = ''; itemsWrapper.style.width = ''; itemsWrapper.style.marginTop = ''; itemsWrapper.style.minHeight = ''; // Remove min-height when populated sortedFavorites.forEach(fav => { itemsWrapper.appendChild(createFavoriteItemElement(fav)); }); } logDebug(`Successfully displayed ${sortedFavorites.length} favorites.`); // After displaying favorites, check if we need to scroll back // Add a 1-second delay before attempting to scroll if (clickedFavoriteAppId !== null) { logDebug('Clicked favorite detected. Waiting 1 second before scrolling back.'); setTimeout(() => { scrollToClickedFavorite(); }, 1000); // 1000 milliseconds = 1 second } } catch (error) { logError('Error displaying favorites:', error); itemsWrapper.textContent = 'Error loading favorites.'; itemsWrapper.style.color = 'red'; itemsWrapper.style.textAlign = 'center'; itemsWrapper.style.width = '100%'; itemsWrapper.style.marginTop = '10px'; itemsWrapper.style.minHeight = '100px'; // Give it a minimum height } } function scrollToClickedFavorite() { if (clickedFavoriteAppId === null) { logDebug('No favorite item click recorded to scroll back to.'); return; } logDebug(`Attempting to scroll back to favorite App ID ${clickedFavoriteAppId}, Foil: ${clickedFavoriteIsFoil}`); // Find the element corresponding to the clicked favorite const selector = `.favorite-item[data-appid="${clickedFavoriteAppId}"][data-is-foil="${clickedFavoriteIsFoil}"]`; const clickedElement = document.querySelector(selector); if (clickedElement) { logDebug('Found clicked favorite element, scrolling into view.'); // Use scrollIntoView with options for smoother scrolling clickedElement.scrollIntoView({ behavior: 'smooth', // Use smooth scrolling block: 'center' // Align the element to the center of the viewport }); } else { logWarn(`Clicked favorite element not found after update: ${selector}`); // If the element wasn't found (e.g., it was just deleted), // we could potentially scroll back to the saved scroll position, // but scrolling into view of the item is generally better. // For now, just log a warning. } // Reset the global state after attempting to scroll clickedFavoriteAppId = null; clickedFavoriteIsFoil = null; scrollPositionBeforeUpdate = 0; // Reset scroll position too } // --- Import/Export Functions --- async function exportFavorites() { logDebug('Attempting to export favorites...'); try { const favorites = await getFavorites(); // Exclude the 'id' field for cleaner export, as it's derived const exportableFavorites = favorites.map(fav => ({ appId: fav.appId, name: fav.name, imageUrl: fav.imageUrl, isFoil: fav.isFoil })); const jsonString = JSON.stringify(exportableFavorites, null, 2); // Pretty print JSON // Display the JSON to the user displayExportJson(jsonString); } catch (error) { logError('Error exporting favorites:', error); alert('Error exporting favorites. Check console for details.'); } } function displayExportJson(jsonString) { // Create a modal-like overlay or inject into a dedicated area let exportArea = document.getElementById('favorites-export-area'); if (!exportArea) { exportArea = document.createElement('div'); exportArea.id = 'favorites-export-area'; exportArea.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 1000; background: #1b2838; /* Steam dark background */ border: 1px solid #333; padding: 20px; border-radius: 5px; box-shadow: 0 0 20px rgba(0,0,0,0.5); color: #B8B6B4; width: 80%; max-width: 600px; max-height: 80%; overflow-y: auto; `; document.body.appendChild(exportArea); const closeButton = document.createElement('span'); closeButton.textContent = 'X'; closeButton.style.cssText = ` position: absolute; top: 10px; right: 10px; font-size: 18px; cursor: pointer; color: #B8B6B4; `; closeButton.onclick = () => exportArea.remove(); exportArea.appendChild(closeButton); const title = document.createElement('h3'); title.textContent = 'Exported Favorites (Copy Below)'; title.style.color = '#8BC53F'; title.style.marginTop = '0'; exportArea.appendChild(title); const textArea = document.createElement('textarea'); textArea.style.cssText = ` width: 100%; height: 200px; margin-top: 10px; background: #000; color: #B8B6B4; border: 1px solid #333; padding: 10px; resize: vertical; font-family: monospace; `; textArea.value = jsonString; exportArea.appendChild(textArea); // Select text on focus for easy copying textArea.onfocus = () => { textArea.select(); }; // Also select on click for mobile or quick copy textArea.onclick = () => { textArea.select(); }; } else { // If area already exists, just update the text area content const textArea = exportArea.querySelector('textarea'); if (textArea) { textArea.value = jsonString; textArea.focus(); textArea.select(); // Select for easy copying } exportArea.style.display = 'block'; // Show if hidden } } function showImportArea() { // Create a modal-like overlay or inject into a dedicated area let importArea = document.getElementById('favorites-import-area'); if (!importArea) { importArea = document.createElement('div'); importArea.id = 'favorites-import-area'; importArea.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 1000; background: #1b2838; /* Steam dark background */ border: 1px solid #333; padding: 20px; border-radius: 5px; box-shadow: 0 0 20px rgba(0,0,0,0.5); color: #B8B6B4; width: 80%; max-width: 600px; max-height: 80%; overflow-y: auto; `; document.body.appendChild(importArea); const closeButton = document.createElement('span'); closeButton.textContent = 'X'; closeButton.style.cssText = ` position: absolute; top: 10px; right: 10px; font-size: 18px; cursor: pointer; color: #B8B6B4; `; closeButton.onclick = () => importArea.remove(); importArea.appendChild(closeButton); const title = document.createElement('h3'); title.textContent = 'Import Favorites (Paste JSON Below)'; title.style.color = '#8BC53F'; title.style.marginTop = '0'; importArea.appendChild(title); const textArea = document.createElement('textarea'); textArea.id = 'favorites-import-textarea'; textArea.style.cssText = ` width: 100%; height: 200px; margin-top: 10px; background: #000; color: #B8B6B4; border: 1px solid #333; padding: 10px; resize: vertical; font-family: monospace; `; importArea.appendChild(textArea); const importButton = document.createElement('button'); importButton.className = 'btn_green_steamui btn_medium'; // Steam button style importButton.style.marginTop = '15px'; importButton.textContent = 'Import Favorites'; importButton.onclick = () => importFavoritesFromTextarea(); importArea.appendChild(importButton); const warning = document.createElement('p'); warning.style.fontSize = '11px'; warning.style.color = '#ff6600'; // Orange color warning.textContent = 'Warning: Importing favorites will add them to your existing list. Duplicate items will be updated.'; importArea.appendChild(warning); } else { // If area already exists, clear textarea and show const textArea = importArea.querySelector('textarea'); if (textArea) { textArea.value = ''; } importArea.style.display = 'block'; // Show if hidden } } async function importFavoritesFromTextarea() { logDebug('Attempting to import favorites from textarea...'); const importArea = document.getElementById('favorites-import-area'); const textArea = document.getElementById('favorites-import-textarea'); if (!textArea || !importArea) { logError('Import area or textarea not found.'); return; } const jsonString = textArea.value.trim(); if (!jsonString) { alert('Please paste the JSON data into the text area.'); return; } try { const importedData = JSON.parse(jsonString); if (!Array.isArray(importedData)) { alert('Import failed: Data is not a valid JSON array.'); logError('Import failed: Data is not an array.', importedData); return; } const validFavorites = importedData.filter(item => item && (typeof item.appId === 'number' || typeof item.appId === 'string') && typeof item.name === 'string' && typeof item.imageUrl === 'string' && typeof item.isFoil === 'boolean' ).map(item => ({ // Ensure appId is string for consistency with ID generation appId: String(item.appId), name: item.name, imageUrl: item.imageUrl, isFoil: item.isFoil, // Re-generate the ID for consistency and uniqueness in the DB id: `${String(item.appId)}_${item.isFoil ? 'foil' : 'regular'}` })); if (validFavorites.length === 0) { alert('Import failed: No valid favorite items found in the data.'); logError('Import failed: No valid favorite items found after filtering.', importedData); return; } logDebug(`Attempting to add ${validFavorites.length} valid favorites to IndexedDB.`); const database = await openDatabase(); const transaction = database.transaction(FAVORITES_STORE_NAME, 'readwrite'); const store = transaction.objectStore(FAVORITES_STORE_NAME); let addedCount = 0; let errorCount = 0; validFavorites.forEach(fav => { const request = store.put(fav); // Use put to add or update request.onsuccess = () => { addedCount++; }; request.onerror = (event) => { logError(`Error adding/updating favorite ${fav.id} during import:`, event.target.error); errorCount++; }; }); transaction.oncomplete = () => { log(`Import transaction complete. Processed: ${validFavorites.length}, Errors: ${errorCount}`); alert(`Import complete!\nProcessed: ${validFavorites.length}\nErrors: ${errorCount}\n(Note: Existing items with the same App ID and Foil status were updated.)`); importArea.remove(); // Close modal after import displayFavorites(); // Refresh displayed list }; transaction.onerror = (event) => { logError('Import transaction failed:', event.target.error); alert(`Import failed: Transaction error. Check console.`); // Keep modal open to show error? Or close? Let's close for now. importArea.remove(); displayFavorites(); // Still try to refresh display }; transaction.onabort = (event) => { logWarn('Import transaction aborted:', event.target.error); alert(`Import aborted. Check console.`); importArea.remove(); displayFavorites(); // Still try to refresh display }; } catch (error) { logError('Error parsing or processing imported JSON:', error); alert(`Import failed: Invalid JSON format or processing error.\nDetails: ${error.message}. Check console for more details.`); } } // --- End Import/Export Functions --- const style = ` .badge-container { display: flex; gap: 40px; margin-top: 20px; padding: 10px; background: rgba(0, 0, 0, 0.2); border-radius: 3px; justify-content: center; min-height: 180px; /* Placeholder height for badge container */ align-items: center; /* Center content vertically during loading */ } .badge-list-container, .favorites-container { display: flex; gap: 20px; margin-bottom: 20px; padding: 10px; background: rgba(0, 0, 0, 0.2); border-radius: 3px; justify-content: flex-start; /* Align items to the start */ flex-wrap: wrap; align-items: flex-start; /* Align rows to the start */ min-height: 200px; /* Placeholder height for list containers */ align-content: flex-start; /* Align rows to the start during loading */ } .favorites-container { margin-top: 20px; /* Space above favorites */ flex-direction: column; /* Stack controls and items */ } .favorites-controls { display: flex; gap: 10px; margin-bottom: 10px; align-items: center; flex-wrap: wrap; /* Allow controls to wrap on smaller screens */ /* Added to push groups to edges */ justify-content: space-between; width: 100%; /* Ensure it takes full width */ } /* Group sort controls */ .favorites-controls .sort-group { display: flex; gap: 10px; align-items: center; } /* Group import/export buttons */ .favorites-controls .button-group { display: flex; gap: 10px; align-items: center; } .favorites-controls label { color: #B8B6B4; font-size: 12px; } /* --- Steam-like Dropdown Styling --- */ .favorites-controls .btn_grey_black { padding: 0 8px; /* Adjust padding */ height: 24px; /* Adjust height */ line-height: 24px; /* Center text vertically */ font-size: 12px; /* Match label font size */ position: relative; /* Needed for arrow positioning */ cursor: pointer; background: linear-gradient( to right, #333, #444 ); /* Darker gradient */ border-radius: 3px; border: none; /* Remove default border */ color: #B8B6B4; text-shadow: none; /* Remove default text shadow */ /* Added min-width to ensure text visibility */ min-width: 150px; /* Adjust as needed based on longest option text */ display: inline-block; /* Ensure it behaves like a block for width */ box-sizing: border-box; /* Include padding and border in element's total width and height */ } .favorites-controls .btn_grey_black select { -webkit-appearance: none; /* Remove default dropdown arrow */ -moz-appearance: none; appearance: none; background: transparent; /* Make select background transparent */ border: none; padding: 0; margin: 0; color: #B8B6B4; font-size: 12px; cursor: pointer; outline: none; /* Remove focus outline */ width: 100%; /* Take full width of parent button */ height: 100%; /* Take full height of parent button */ position: absolute; top: 0; left: 0; right: 0; bottom: 0; z-index: 2; /* Place select above the custom arrow */ padding-right: 20px; /* Add padding on the right to make space for the arrow */ } /* Custom arrow */ .favorites-controls .btn_grey_black::after { content: '▼'; /* Unicode down arrow */ position: absolute; top: 50%; right: 5px; /* Position arrow */ transform: translateY(-50%); font-size: 8px; /* Smaller arrow */ color: #B8B6B4; pointer-events: none; /* Allow clicks to pass through to select */ z-index: 1; /* Place arrow below select */ } /* Hover effect for the button */ .favorites-controls .btn_grey_black:hover { background: linear-gradient( to right, #444, #555 ); /* Lighter gradient on hover */ color: #CFCFCF; } /* Focus effect for the button */ .favorites-controls .btn_grey_black:focus-within { outline: 1px solid #8BC53F; /* Green outline on focus */ } .favorites-controls .btn_grey_black option { background-color: #333; /* Background for dropdown options */ color: #B8B6B4; } /* --- End Steam-like Dropdown Styling --- */ /* --- Import/Export Button Styling --- */ .favorites-controls .steam-button { display: inline-block; padding: 0 15px; height: 24px; line-height: 24px; font-size: 12px; text-decoration: none; text-align: center; cursor: pointer; border-radius: 3px; border: none; color: #B8B6B4; background: linear-gradient( to right, #333, #444 ); box-sizing: border-box; } .favorites-controls .steam-button:hover { background: linear-gradient( to right, #444, #555 ); color: #CFCFCF; } .favorites-controls .steam-button:active { background: linear-gradient( to right, #222, #333 ); color: #B8B6B4; } /* --- End Import/Export Button Styling --- */ .favorites-items-wrapper { display: flex; flex-wrap: wrap; gap: 20px; /* Gap between items */ width: 100%; /* Take full width of container */ } .badge-box, .badge-list-box, .favorite-item { flex: 0 0 calc(16.666% - 17px); /* Calculate width for 6 items per row, adjusting for gap */ text-align: center; padding: 15px; border-radius: 5px; background: rgba(0, 0, 0, 0.1); min-width: 100px; max-width: 150px; box-sizing: border-box; display: flex; flex-direction: column; justify-content: space-between; align-items: center; cursor: pointer; /* Indicate clickable */ transition: background-color 0.2s ease; /* Smooth hover effect */ } .favorite-item { flex: 0 0 calc(16.666% - 17px); /* Ensure 6 items per row */ cursor: pointer; } .badge-box:hover, .badge-list-box:hover, .favorite-item:hover { background-color: rgba(255, 255, 255, 0.05); /* Subtle hover effect */ } .badge-box.foil, .badge-list-box.foil, .favorite-item.foil { background: linear-gradient( 45deg, rgba(0, 0, 0, 0.1) 0%, rgba(255, 255, 255, 0.1) 50%, rgba(0, 0, 0, 0.1) 100% ); animation: shine 3000s linear infinite; border: 1px solid rgba(255, 255, 255, 0.1); box-shadow: 0 0 15px rgba(255, 255, 245, 0.1); } @keyframes shine { 0% { background-position: -200% center; } 100% { background-position: 200% center; } } .badge-title, .badge-list-title, .favorite-title { margin-bottom: 10px; color: #8BC53F; font-weight: bold; font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; width: 100%; } .badge-image, .badge-list-image, .favorite-image { max-width: 100%; height: auto; display: block; margin: 0 auto; flex-shrink: 0; } .badge_empty_circle, .badge_list_empty_circle, .favorite_empty_circle { width: 80px; height: 80px; background: rgba(0, 0, 0, 0.2); border-radius: 50%; margin: 0 auto; flex-shrink: 0; } .badge-link, .badge-list-link, .favorite-link { text-decoration: none; color: inherit; display: flex; justify-content: center; align-items: center; width: 100%; min-height: 80px; box-sizing: border-box; margin-bottom: 5px; } .badge-level, .badge-list-scarcity, .favorite-appid { margin-top: 10px; color: #B8B6B4; font-size: 12px; } .badge-list-level { /* New style for level in the list */ margin-top: 4px; color: #8BC53F; font-size: 11px; } .foil .badge-list-level { color: #CFE6F5; } .badge-link:hover, .badge-list-link:hover { opacity: 0.8; } .foil .badge-title, .foil .badge-list-title, .foil .favorite-title { color: #CFE6F5; text-shadow: 0 0 5px rgba(207, 230, 245, 0.5); } .badge-progress { margin-top: 8px; color: #B8B6B4; font-size: 12px; } .badge-sets { margin-top: 4px; color: #8BC53F; font-size: 12px; } .foil .badge-sets { color: #CFE6F5; } .badge-list-scarcity, .favorite-appid { margin-top: auto; font-size: 11px; color: #B8B6B4; } /* New style for unlocked info */ .badge-unlocked { margin-top: 4px; /* Space below progress */ color: #B8B6B4; font-size: 11px; /* Slightly smaller font */ } /* Styles for Import/Export Modals */ #favorites-export-area, #favorites-import-area { /* Added styles in the JS function for positioning and basics */ /* Add more specific styles here if needed */ } `; function addStyle(css) { const styleSheet = document.createElement("style"); styleSheet.textContent = css; // Ensure document.head is available before appending if (document.head) { document.head.appendChild(styleSheet); } else { // Fallback if head is not available immediately document.documentElement.appendChild(styleSheet); } } function getAppId() { const url = window.location.href; const match = url.match(/\d+$/); return match ? match[0] : null; } function parseBadgeData(html, isFoil, userId, isSteamId64, appId) { const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); // Find the main badge info container first for more precise selection // Look for a common parent like .badge_row or .badge_info const mainBadgeInfoArea = doc.querySelector('.badge_row, .badge_info'); // Use comma to try multiple selectors const context = mainBadgeInfoArea || doc; // Use the main area if found, otherwise the whole doc const badgeImage = context.querySelector('.badge_info_image .badge_icon'); const badgeName = context.querySelector('.badge_info_title'); const badgeLevel = context.querySelector('.badge_info_description'); // This might contain "Level X" or other text // <--- Refined: Get unlocked element within the badge_empty_right div inside the context ---> const badgeEmptyRight = context.querySelector('.badge_empty_right'); const badgeUnlocked = badgeEmptyRight ? badgeEmptyRight.querySelector('.badge_info_unlocked') : null; // <--- End Refined ---> const cardElements = doc.querySelectorAll('.badge_card_set_card'); // Card elements are usually in the same doc const cardQuantities = []; const cardInfo = []; // To store card name and quantity together cardElements.forEach(cardElement => { const qtyElement = cardElement.querySelector('.badge_card_set_text_qty'); const nameElement = cardElement.querySelector('.badge_card_set_text_cardname'); const quantityMatch = qtyElement ? qtyElement.textContent.match(/\((\d+)\)/) : null; const quantity = quantityMatch ? parseInt(quantityMatch[1]) : 0; // Treat empty quantity as 0 cardQuantities.push(quantity); const name = nameElement ? nameElement.textContent.trim() : 'Unknown Card'; cardInfo.push({ name: name, quantity: quantity }); }); logDebug(`${isFoil ? 'Foil' : 'Regular'} Badge Cards Found:`); logDebug(cardInfo); // Use logDebug for potentially large data const totalCards = cardElements.length; const completeSets = cardQuantities.length > 0 ? Math.min(...cardQuantities) : 0; const ownedCards = cardQuantities.filter(qty => qty > 0).length; logDebug(`${isFoil ? 'Foil' : 'Regular'} Badge Summary:`); logDebug(` Total Cards in Set: ${totalCards}`); logDebug(` Owned Card Types: ${ownedCards}`); logDebug(` Complete Sets: ${completeSets}`); const levelMatch = badgeLevel ? badgeLevel.textContent.match(/Level (\d+)/) : null; // Construct the badge URL using the provided ID and type const badgeBaseUrl = userId ? `https://steamcommunity.com/${isSteamId64 ? 'profiles' : 'id'}/${userId}/gamecards/${appId}` : null; const badgeUrl = badgeBaseUrl ? `${badgeBaseUrl}${isFoil ? '?border=1' : ''}` : null; return { imageUrl: badgeImage ? badgeImage.src : null, name: badgeName ? badgeName.textContent.trim() : null, level: levelMatch ? levelMatch[1] : null, badgeUrl: badgeUrl, cardProgress: { owned: ownedCards, total: totalCards, completeSets: completeSets }, unlockedInfo: badgeUnlocked ? badgeUnlocked.textContent.trim() : null // Extract unlocked info }; } function createBadgeElement(badgeData, isFoil) { const badgeBox = document.createElement('div'); badgeBox.className = `badge-box ${isFoil ? 'foil' : ''}`; const title = document.createElement('div'); title.className = 'badge-title'; title.textContent = isFoil ? 'Foil Badge' : 'Regular Badge'; badgeBox.appendChild(title); const link = document.createElement('a'); link.className = 'badge-link'; if (badgeData.badgeUrl) { link.href = badgeData.badgeUrl; link.target = '_blank'; // Open link in a new tab } else { link.style.cursor = 'default'; } if (badgeData.imageUrl) { const image = document.createElement('img'); image.className = 'badge-image'; image.src = badgeData.imageUrl; image.alt = badgeData.name || 'Badge Image'; // Add alt text link.appendChild(image); } else { const emptyCircle = document.createElement('div'); emptyCircle.className = 'badge_empty_circle'; link.appendChild(emptyCircle); } badgeBox.appendChild(link); if (badgeData.level) { const level = document.createElement('div'); level.className = 'badge-level'; level.textContent = `Level ${badgeData.level}`; badgeBox.appendChild(level); } if (badgeData.cardProgress) { const progress = document.createElement('div'); progress.className = 'badge-progress'; if (badgeData.cardProgress.total > 0) { if (badgeData.cardProgress.owned === 0 || badgeData.cardProgress.owned === badgeData.cardProgress.total) { progress.textContent = ''; // No progress text if 0/total or total/total } else { progress.textContent = `${badgeData.cardProgress.owned}/${badgeData.cardProgress.total} Cards`; } if (badgeData.cardProgress.completeSets > 0) { const sets = document.createElement('div'); sets.className = 'badge-sets'; sets.textContent = `${badgeData.cardProgress.completeSets} Complete ${badgeData.cardProgress.completeSets === 1 ? 'Set' : 'Sets'}`; progress.appendChild(sets); } } else { progress.textContent = 'No cards in this set'; } badgeBox.appendChild(progress); } // Add unlocked info below progress if (badgeData.unlockedInfo) { const unlocked = document.createElement('div'); unlocked.className = 'badge-unlocked'; unlocked.textContent = badgeData.unlockedInfo; badgeBox.appendChild(unlocked); } return badgeBox; } function createBadgeListItem(badgeData) { const badgeListBox = document.createElement('div'); badgeListBox.className = `badge-list-box ${badgeData.isFoil ? 'foil' : ''}`; // Store data attributes for easy access when clicking badgeListBox.dataset.appid = badgeData.appId; badgeListBox.dataset.badgeImage = badgeData.badgeImage; badgeListBox.dataset.isFoil = badgeData.isFoil; badgeListBox.dataset.name = badgeData.name || 'Unknown Badge'; const title = document.createElement('div'); title.className = 'badge-list-title'; title.textContent = badgeData.name || 'Unknown Badge'; badgeListBox.appendChild(title); const link = document.createElement('a'); link.className = 'badge-list-link'; link.style.cursor = 'pointer'; // Make it look clickable let badgeImageUrl = null; if (badgeData.appId && badgeData.badgeImage && (typeof badgeData.appId === 'number' || typeof badgeData.appId === 'string') && typeof badgeData.badgeImage === 'string' && badgeData.badgeImage.length > 0) { badgeImageUrl = `https://cdn.cloudflare.steamstatic.com/steamcommunity/public/images/items/${badgeData.appId}/${badgeData.badgeImage}`; badgeListBox.dataset.imageUrl = badgeImageUrl; // Store image URL const image = document.createElement('img'); image.className = 'badge-list-image'; image.src = badgeImageUrl; image.alt = badgeData.name || 'Badge Image'; image.onerror = () => { logError(`Failed to load constructed image for badge list item: ${badgeData.name} from URL: ${badgeImageUrl}`); link.innerHTML = ''; const emptyCircle = document.createElement('div'); emptyCircle.className = 'badge_list_empty_circle'; link.appendChild(emptyCircle); badgeListBox.dataset.imageUrl = ''; // Clear failed image URL }; link.appendChild(image); } else { const emptyCircle = document.createElement('div'); emptyCircle.className = 'badge_list_empty_circle'; link.appendChild(emptyCircle); badgeListBox.dataset.imageUrl = ''; // Store empty image URL } badgeListBox.appendChild(link); // Display scarcity const scarcity = document.createElement('div'); scarcity.className = 'badge-list-scarcity'; if (badgeData.scarcity !== undefined && badgeData.scarcity !== null) { scarcity.textContent = `Scarcity: ${badgeData.scarcity.toFixed(0)}`; // Format scarcity to 0 decimal places if (badgeData.isFoil) { scarcity.style.color = '#CFE6F5'; // Foil color for foil scarcity } } else { scarcity.textContent = badgeData.isFoil ? 'Scarcity (Foil): N/A' : 'Scarcity: N/A'; // Indicate foil or N/A scarcity.style.fontStyle = 'italic'; scarcity.style.color = badgeData.isFoil ? '#CFE6F5' : '#666'; // Foil color or grey for N/A } badgeListBox.appendChild(scarcity); // Display level for non-foil badges if available if (!badgeData.isFoil && badgeData.highestLevel !== undefined && badgeData.highestLevel !== null) { const level = document.createElement('div'); level.className = 'badge-list-level'; level.textContent = `Level ${badgeData.highestLevel}`; badgeListBox.appendChild(level); } // Add click listener to save/remove from favorites badgeListBox.addEventListener('click', () => { toggleFavorite({ appId: badgeData.appId, name: badgeData.name || 'Unknown Badge', imageUrl: badgeListBox.dataset.imageUrl, // Use stored URL isFoil: badgeData.isFoil === 'true' || badgeData.isFoil === true // Ensure boolean }); }); return badgeListBox; } function createFavoriteItemElement(favoriteData) { const favoriteItem = document.createElement('div'); favoriteItem.className = `favorite-item ${favoriteData.isFoil ? 'foil' : ''}`; favoriteItem.dataset.appid = favoriteData.appId; favoriteItem.dataset.isFoil = favoriteData.isFoil; const title = document.createElement('div'); title.className = 'favorite-title'; title.textContent = favoriteData.name || `App ${favoriteData.appId}`; favoriteItem.appendChild(title); const link = document.createElement('a'); link.className = 'favorite-link'; link.href = `#${favoriteData.appId}`; // Set the URL hash link.addEventListener('click', (event) => { event.preventDefault(); // Prevent default link behavior // --- Store scroll position and clicked item info --- scrollPositionBeforeUpdate = window.scrollY || document.documentElement.scrollTop; clickedFavoriteAppId = favoriteData.appId; clickedFavoriteIsFoil = favoriteData.isFoil; logDebug(`Clicked favorite item: App ID ${clickedFavoriteAppId}, Foil: ${clickedFavoriteIsFoil}. Saved scroll position: ${scrollPositionBeforeUpdate}`); // --- End Store --- // Change the URL hash without a full page reload window.location.hash = `${favoriteData.appId}`; // Manually trigger the update as hash change might not trigger MutationObserver reliably across all browsers/frameworks setTimeout(updateBadgeInfo, 100); // Add a small delay }); if (favoriteData.imageUrl) { const image = document.createElement('img'); image.className = 'favorite-image'; image.src = favoriteData.imageUrl; image.alt = favoriteData.name || 'Badge Image'; image.onerror = () => { logError(`Failed to load image for favorite item: ${favoriteData.name} from URL: ${favoriteData.imageUrl}`); link.innerHTML = ''; // Clear any failed image const emptyCircle = document.createElement('div'); emptyCircle.className = 'favorite_empty_circle'; link.appendChild(emptyCircle); }; link.appendChild(image); } else { const emptyCircle = document.createElement('div'); emptyCircle.className = 'favorite_empty_circle'; link.appendChild(emptyCircle); } favoriteItem.appendChild(link); const appidElement = document.createElement('div'); appidElement.className = 'favorite-appid'; appidElement.textContent = `App ID: ${favoriteData.appId}`; if (favoriteData.isFoil) { const foilIndicator = document.createElement('span'); foilIndicator.textContent = ' (Foil)'; foilIndicator.style.color = '#CFE6F5'; appidElement.appendChild(foilIndicator); } favoriteItem.appendChild(appidElement); // Add a click listener specifically for removing the item // This listener is on the item itself. We now check if the clicked // element is the title or part of the link before removing. favoriteItem.addEventListener('click', (event) => { const clickedElement = event.target; // Check if the clicked element is the title OR is inside the link const isTitle = clickedElement.classList.contains('favorite-title'); const isInsideLink = clickedElement.closest('.favorite-link'); const isAppID = clickedElement.closest('.favorite-appid'); if (!isTitle && !isInsideLink && !isAppID) { logDebug('Clicked outside the title and link, attempting to remove favorite.'); toggleFavorite({ appId: favoriteData.appId, name: favoriteData.name, imageUrl: favoriteData.imageUrl, isFoil: favoriteData.isFoil }); } else if (isTitle) { logDebug('Clicked on the favorite title, not removing favorite.'); } else if (isInsideLink) { logDebug('Clicked on the favorite link, not removing favorite.'); }else if (isAppID) { logDebug('Clicked on the favorite link, not removing favorite.'); } }); return favoriteItem; } async function fetchAndDisplayBadgeList(appId) { if (!SteamSetsAPI || SteamSetsAPI === "ss_YOUR_API_KEY") { logWarn("SteamSets API key not configured or is placeholder. Skipping badge list fetch."); let badgeListContainer = document.querySelector('.badge-list-container'); if (!badgeListContainer) { badgeListContainer = document.createElement('div'); badgeListContainer.className = 'badge-list-container'; const target = document.querySelector('.booster_creator_left'); if (target) { target.insertAdjacentElement('afterend', badgeListContainer); } } if (badgeListContainer) { badgeListContainer.innerHTML = ''; badgeListContainer.textContent = 'SteamSets API key is not set. Badge list unavailable.'; badgeListContainer.style.color = 'orange'; badgeListContainer.style.textAlign = 'center'; badgeListContainer.style.minHeight = '100px'; // Keep a minimum height for the message badgeListContainer.style.display = 'flex'; // Ensure flex properties apply for centering badgeListContainer.style.alignItems = 'center'; badgeListContainer.style.justifyContent = 'center'; } return; } log(`Attempting to fetch badge list for App ID: ${appId}. Checking cache first.`); let badgeListContainer = document.querySelector('.badge-list-container'); if (!badgeListContainer) { badgeListContainer = document.createElement('div'); badgeListContainer.className = 'badge-list-container'; const target = document.querySelector('.booster_creator_left'); if (target) { target.insertAdjacentElement('afterend', badgeListContainer); } else { logWarn('Target element .booster_creator_left not found for badge list container insertion.'); return; } } badgeListContainer.innerHTML = ''; // Clear previous content // Temporarily add a loading message and apply centering styles const loadingMessage = document.createElement('div'); loadingMessage.textContent = 'Loading available badges...'; loadingMessage.style.color = '#B8B6B4'; loadingMessage.style.textAlign = 'center'; loadingMessage.style.width = '100%'; // Ensure text centers horizontally badgeListContainer.appendChild(loadingMessage); // Apply centering to the container itself while loading badgeListContainer.style.display = 'flex'; badgeListContainer.style.alignItems = 'center'; badgeListContainer.style.justifyContent = 'center'; badgeListContainer.style.flexWrap = 'wrap'; // Keep wrap for items later try { const cachedData = await getCachedData(appId); // <-- Await the promise here if (cachedData) { logDebug(`Using cached data for App ID ${appId}.`); displayBadgeList(cachedData, appId); // Reset centering styles once content is loaded badgeListContainer.style.display = 'flex'; // Keep flex for items badgeListContainer.style.alignItems = 'flex-start'; // Reset alignment badgeListContainer.style.justifyContent = 'flex-start'; // Reset alignment badgeListContainer.style.color = ''; // Reset style badgeListContainer.style.textAlign = ''; } else { log(`Fetching badge list for App ID: ${appId} from SteamSets API.`); loadingMessage.textContent = 'Fetching available badges...'; // Update loading state GM_xmlhttpRequest({ method: 'POST', url: 'https://api.steamsets.com/v1/app.listBadges', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${SteamSetsAPI}` }, data: JSON.stringify({ appId: parseInt(appId) // Ensure appId is an integer }), onload: response => { log(`SteamSets API response status: ${response.status}`); badgeListContainer.innerHTML = ''; // Clear loading message if (response.status === 200) { try { const data = JSON.parse(response.responseText); if (data && data.badges && Array.isArray(data.badges)) { log(`Fetched ${data.badges.length} badges from SteamSets API.`); // Cache the successful response cacheData(appId, data.badges).catch(cacheError => { logError('Error caching data after successful API fetch:', cacheError); // Continue displaying even if caching fails }); displayBadgeList(data.badges, appId); // Reset centering styles badgeListContainer.style.display = 'flex'; badgeListContainer.style.alignItems = 'flex-start'; badgeListContainer.style.justifyContent = 'flex-start'; badgeListContainer.style.color = ''; badgeListContainer.style.textAlign = ''; } else { logError('SteamSets API response did not contain expected badge data structure:', data); badgeListContainer.textContent = 'Error fetching badge list: Invalid data format.'; badgeListContainer.style.color = 'orange'; badgeListContainer.style.textAlign = 'center'; // Keep centering for error message badgeListContainer.style.display = 'flex'; badgeListContainer.style.alignItems = 'center'; badgeListContainer.style.justifyContent = 'center'; } } catch (e) { logError('Error parsing SteamSets API response:', e); badgeListContainer.textContent = 'Error fetching badge list: Invalid JSON response.'; badgeListContainer.style.color = 'orange'; badgeListContainer.style.textAlign = 'center'; // Keep centering for error message badgeListContainer.style.display = 'flex'; badgeListContainer.style.alignItems = 'center'; badgeListContainer.style.justifyContent = 'center'; } } else if (response.status === 401) { logError(`SteamSets API request failed with status 401 (Unauthorized). Check your API key.`, response.responseText); badgeListContainer.innerHTML = ''; badgeListContainer.textContent = 'SteamSets API Error: Unauthorized (401). Please check your API key configuration.'; badgeListContainer.style.color = 'red'; badgeListContainer.style.textAlign = 'center'; // Keep centering for error message badgeListContainer.style.display = 'flex'; badgeListContainer.style.alignItems = 'center'; badgeListContainer.style.justifyContent = 'center'; } else { logError(`SteamSets API request failed with status: ${response.status}`, response.responseText); let errorMessage = `Error fetching badge list. Status: ${response.status}.`; if (response.responseText) { try { const errorData = JSON.parse(response.responseText); if (errorData.message) { errorMessage += ` Message: ${errorData.message}`; } } catch (e) { // Ignore JSON parsing error if responseText is not JSON } } badgeListContainer.innerHTML = ''; badgeListContainer.textContent = errorMessage; badgeListContainer.style.color = 'orange'; badgeListContainer.style.textAlign = 'center'; // Keep centering for error message badgeListContainer.style.display = 'flex'; badgeListContainer.style.alignItems = 'center'; badgeListContainer.style.justifyContent = 'center'; } }, onerror: (error) => { logError(`SteamSets API request failed:`, error); badgeListContainer.innerHTML = ''; // Clear loading message badgeListContainer.textContent = `Network error fetching badge list.`; badgeListContainer.style.color = 'red'; badgeListContainer.style.textAlign = 'center'; // Keep centering for error message badgeListContainer.style.display = 'flex'; badgeListContainer.style.alignItems = 'center'; badgeListContainer.style.justifyContent = 'center'; } }); } } catch (error) { // Catch errors from openDatabase or getCachedData logError('Error during API cache retrieval or database open:', error); badgeListContainer.innerHTML = ''; // Clear loading message badgeListContainer.textContent = `Error accessing badge data cache.`; badgeListContainer.style.color = 'red'; badgeListContainer.style.textAlign = 'center'; // Keep centering for error message badgeListContainer.style.display = 'flex'; badgeListContainer.style.alignItems = 'center'; badgeListContainer.style.justifyContent = 'center'; } } function displayBadgeList(badges, appId) { const badgeListContainer = document.querySelector('.badge-list-container'); if (!badgeListContainer) { logWarn('Badge list container not found during display.'); return; } badgeListContainer.innerHTML = ''; // Clear existing content if (!badges || badges.length === 0) { badgeListContainer.textContent = 'No badges found for this app via SteamSets API.'; badgeListContainer.style.color = '#B8B6B4'; badgeListContainer.style.textAlign = 'center'; // Keep centering for message badgeListContainer.style.display = 'flex'; badgeListContainer.style.alignItems = 'center'; badgeListContainer.style.justifyContent = 'center'; return; } // Sort badges const sortedBadges = badges.sort((a, b) => { // Sort non-foil badges first if (a.isFoil !== b.isFoil) { return a.isFoil ? 1 : -1; // non-foil first } // If both are non-foil, sort by highestLevel ascending (1 to 5) if (!a.isFoil && !b.isFoil) { // Handle cases where highestLevel might be missing or null const levelA = a.highestLevel !== undefined && a.highestLevel !== null ? a.highestLevel : Infinity; const levelB = b.highestLevel !== undefined && b.highestLevel !== null ? b.highestLevel : Infinity; if (levelA !== levelB) { return levelA - levelB; } // If levels are the same or missing, sort by scarcity const scarcityA = a.scarcity !== undefined && a.scarcity !== null ? a.scarcity : Infinity; const scarcityB = b.scarcity !== undefined && b.scarcity !== null ? b.scarcity : Infinity; if (scarcityA !== scarcityB) { return scarcityA - scarcityB; } } // If both are foil, sort by rarity ascending if available, otherwise scarcity, otherwise name if (a.isFoil && b.isFoil) { const rarityA = a.rarity !== undefined && a.rarity !== null ? a.rarity : Infinity; const rarityB = b.rarity !== undefined && b.rarity !== null ? b.rarity : Infinity; if (rarityA !== rarityB) { return rarityA - rarityB; } const scarcityA = a.scarcity !== undefined && a.scarcity !== null ? a.scarcity : Infinity; const scarcityB = b.scarcity !== undefined && b.scarcity !== null ? b.scarcity : Infinity; if (scarcityA !== scarcityB) { return scarcityA - scarcityB; } } // Fallback: Sort by name if other criteria are the same or missing return (a.name || '').localeCompare(b.name || ''); }); logDebug(`Displaying all available badges (${sortedBadges.length}):`); sortedBadges.forEach(badge => { // Add appId to the badge data before creating the item badge.appId = appId; badgeListContainer.appendChild(createBadgeListItem(badge)); }); // Reset centering styles for displaying items badgeListContainer.style.display = 'flex'; badgeListContainer.style.alignItems = 'flex-start'; badgeListContainer.style.justifyContent = 'flex-start'; badgeListContainer.style.color = ''; // Reset style badgeListContainer.style.textAlign = ''; } // Main function to fetch and display badge info async function updateBadgeInfo() { const appId = getAppId(); if (!appId) { logWarn('Could not get App ID from URL. Displaying favorites only.'); // If no App ID, clear badge containers and only show favorites clearBadgeContainers(false); // Don't remove favorites container addFavoritesContainerWithControls(); // Ensure favorites container is present displayFavorites(); // Display favorites even if no app ID return; } const userId = USER_STEAM_ID; const isSteamId64 = STEAM_ID_IS_STEAMID64; if (userId === "REPLACE_WITH_YOUR_STEAM_ID" || SteamSetsAPI === "ss_YOUR_API_KEY") { logError("Please update the USER_STEAM_ID and/or SteamSetsAPI variables in the script."); const errorDiv = document.createElement('div'); errorDiv.style.cssText = 'color: red; font-weight: bold; margin-top: 20px; text-align: center;'; errorDiv.textContent = "Steam Badge Info Injector: Please edit the script and update the configuration variables."; const target = document.querySelector('.booster_creator_left'); if (target) { target.insertAdjacentElement('afterend', errorDiv); } else { document.body.prepend(errorDiv); } clearBadgeContainers(false); // Clear any old containers except favorites addFavoritesContainerWithControls(); // Ensure favorites container is present displayFavorites(); // Still show favorites if available return; } // Remove existing containers before adding new ones (except favorites) clearBadgeContainers(true); // Remove badge containers log(`Fetching badge data for App ID: ${appId} and User ID: ${userId} (Type: ${isSteamId64 ? 'SteamID64' : 'Custom ID'})`); // Create and insert the badge list container first with loading state const badgeListContainer = document.createElement('div'); badgeListContainer.className = 'badge-list-container'; badgeListContainer.textContent = 'Loading available badges...'; // Initial loading state // Apply centering and min-height immediately badgeListContainer.style.display = 'flex'; badgeListContainer.style.alignItems = 'center'; badgeListContainer.style.justifyContent = 'center'; badgeListContainer.style.flexWrap = 'wrap'; // Keep wrap for items later const target = document.querySelector('.booster_creator_left'); if (!target) { logWarn('Target element .booster_creator_left not found. Containers not inserted.'); // Still display favorites even if main container fails addFavoritesContainerWithControls(); // Ensure favorites container is present displayFavorites(); return; } target.insertAdjacentElement('afterend', badgeListContainer); // Insert list container // Create and insert the main badge container with loading state const mainContainer = document.createElement('div'); mainContainer.className = 'badge-container'; mainContainer.textContent = 'Loading your badge info...'; // Initial loading state // Apply centering and min-height immediately mainContainer.style.display = 'flex'; mainContainer.style.alignItems = 'center'; mainContainer.style.justifyContent = 'center'; // Insert the main container after the badge list container badgeListContainer.insertAdjacentElement('afterend', mainContainer); // Create and insert the favorites container if it doesn't exist addFavoritesContainerWithControls(); // Always display favorites when updateBadgeInfo runs displayFavorites(); // Fetch and display the list of available badges (this will update the badgeListContainer) fetchAndDisplayBadgeList(appId); // This function now handles updating the content and styles // Construct URLs using the provided user ID and type const userPath = isSteamId64 ? 'profiles' : 'id'; const regularUrl = `https://steamcommunity.com/${userPath}/${userId}/gamecards/${appId}`; const foilUrl = `https://steamcommunity.com/${userPath}/${userId}/gamecards/${appId}?border=1`; Promise.all([ new Promise(resolve => { GM_xmlhttpRequest({ method: 'GET', url: regularUrl, onload: response => { log(`Regular badge request status: ${response.status}`); if (response.status === 200) { resolve(parseBadgeData(response.responseText, false, userId, isSteamId64, appId)); } else { logError(`Failed to load regular badge (Status ${response.status})`); resolve({ imageUrl: null, name: `Error loading regular badge (${response.status})`, level: null, badgeUrl: regularUrl, cardProgress: null, unlockedInfo: null }); // Include unlockedInfo } }, onerror: (error) => { logError(`Error loading regular badge:`, error); resolve({ imageUrl: null, name: 'Error loading regular badge', level: null, badgeUrl: regularUrl, cardProgress: null, unlockedInfo: null }); // Include unlockedInfo } }); }), new Promise(resolve => { GM_xmlhttpRequest({ method: 'GET', url: foilUrl, onload: response => { log(`Foil badge request status: ${response.status}`); if (response.status === 200) { resolve(parseBadgeData(response.responseText, true, userId, isSteamId64, appId)); } else { logError(`Failed to load foil badge (Status ${response.status})`); resolve({ imageUrl: null, name: `Error loading foil badge (${response.status})`, level: null, badgeUrl: foilUrl, cardProgress: null, unlockedInfo: null }); // Include unlockedInfo } }, onerror: (error) => { logError(`Error loading foil badge:`, error); resolve({ imageUrl: null, name: 'Error loading foil badge', level: null, badgeUrl: foilUrl, cardProgress: null, unlockedInfo: null }); // Include unlockedInfo } }); }) ]).then(([regularBadge, foilBadge]) => { mainContainer.innerHTML = ''; // Clear loading message mainContainer.appendChild(createBadgeElement(regularBadge, false)); mainContainer.appendChild(createBadgeElement(foilBadge, true)); // Reset centering styles once content is loaded mainContainer.style.display = 'flex'; // Keep flex mainContainer.style.alignItems = 'center'; // Reset alignment (items are centered by default in flex row) mainContainer.style.justifyContent = 'center'; // Reset alignment (items are centered by default in flex row) }).catch(error => { logError('Error in Promise.all fetching badge data:', error); mainContainer.innerHTML = ''; // Clear loading message mainContainer.textContent = 'Error loading your badge info.'; // Keep centering for error message mainContainer.style.display = 'flex'; mainContainer.style.alignItems = 'center'; mainContainer.style.justifyContent = 'center'; }); } function clearBadgeContainers(removeFavorites = false) { const existingMainContainer = document.querySelector('.badge-container'); if (existingMainContainer) { existingMainContainer.remove(); } const existingListContainer = document.querySelector('.badge-list-container'); if (existingListContainer) { existingListContainer.remove(); } if (removeFavorites) { const existingFavoritesContainer = document.querySelector('.favorites-container'); if (existingFavoritesContainer) { existingFavoritesContainer.remove(); } } const existingError = document.querySelector('div[style*="color: red"], div[style*="color: orange"]'); if(existingError) { existingError.remove(); } } function addFavoritesContainerWithControls() { let favoritesContainer = document.querySelector('.favorites-container'); if (!favoritesContainer) { logDebug('Favorites container not found, creating and adding controls.'); favoritesContainer = document.createElement('div'); favoritesContainer.className = 'favorites-container'; // Add sort controls const controlsDiv = document.createElement('div'); controlsDiv.className = 'favorites-controls'; // Group sort controls const sortGroup = document.createElement('div'); sortGroup.className = 'sort-group'; const label = document.createElement('label'); label.textContent = 'Sort Favorites:'; label.htmlFor = 'favorites-sort'; sortGroup.appendChild(label); // Create the button wrapper for the select const selectWrapper = document.createElement('div'); selectWrapper.className = 'btn_grey_black'; // Apply Steam button class const select = document.createElement('select'); select.id = 'favorites-sort'; select.innerHTML = ` <option value="appid_asc">App ID (Asc)</option> <option value="appid_desc">App ID (Desc)</option> <option value="foil_first">Foil First</option> <!-- Corrected order based on common preference --> <option value="foil_last">Foil Last</option> `; select.value = currentFavoritesSortOrder; // Set initial value select.addEventListener('change', (event) => { currentFavoritesSortOrder = event.target.value; GM_setValue('favoritesSortOrder', currentFavoritesSortOrder); // Save sort order displayFavorites(); // Redraw favorites with new sort order }); selectWrapper.appendChild(select); // Append select inside the button wrapper sortGroup.appendChild(selectWrapper); // Append button wrapper to sort group controlsDiv.appendChild(sortGroup); // Add sort group to controls // Group import/export buttons const buttonGroup = document.createElement('div'); buttonGroup.className = 'button-group'; // Add Export Button const exportButton = document.createElement('button'); exportButton.className = 'steam-button'; // Custom Steam-like style exportButton.textContent = 'Export Favorites'; exportButton.onclick = exportFavorites; buttonGroup.appendChild(exportButton); // Add Import Button const importButton = document.createElement('button'); importButton.className = 'steam-button'; // Custom Steam-like style importButton.textContent = 'Import Favorites'; importButton.onclick = showImportArea; // Show the import modal buttonGroup.appendChild(importButton); controlsDiv.appendChild(buttonGroup); // Add button group to controls favoritesContainer.appendChild(controlsDiv); // Find a suitable insertion point const mainContainer = document.querySelector('.badge-container'); const listContainer = document.querySelector('.badge-list-container'); const target = document.querySelector('.booster_creator_left'); if (mainContainer) { mainContainer.insertAdjacentElement('afterend', favoritesContainer); logDebug('Favorites container inserted after main badge container.'); } else if (listContainer) { listContainer.insertAdjacentElement('afterend', favoritesContainer); logDebug('Favorites container inserted after badge list container.'); } else if (target) { target.insertAdjacentElement('afterend', favoritesContainer); logDebug('Favorites container inserted after .booster_creator_left.'); } else { logWarn('Could not find insertion point for favorites container. Favorites will not be displayed.'); return; // Don't proceed if no target found } logDebug('Favorites container and controls added.'); } else { // If container exists, just ensure the sort select value is correct logDebug('Favorites container already exists, ensuring sort select value is correct.'); const sortSelect = favoritesContainer.querySelector('#favorites-sort'); if (sortSelect) { sortSelect.value = currentFavoritesSortOrder; } // Ensure controls structure is correct (especially after updates) const controlsDiv = favoritesContainer.querySelector('.favorites-controls'); if (controlsDiv) { // Check if the grouping divs exist, if not, rebuild the controls if (!controlsDiv.querySelector('.sort-group') || !controlsDiv.querySelector('.button-group')) { logDebug('Favorites controls structure outdated, rebuilding controls div.'); // Clear existing controls controlsDiv.innerHTML = ''; // Rebuild sort group const sortGroup = document.createElement('div'); sortGroup.className = 'sort-group'; const label = document.createElement('label'); label.textContent = 'Sort Favorites:'; label.htmlFor = 'favorites-sort'; sortGroup.appendChild(label); const selectWrapper = document.createElement('div'); selectWrapper.className = 'btn_grey_black'; const select = document.createElement('select'); select.id = 'favorites-sort'; select.innerHTML = ` <option value="appid_asc">App ID (Asc)</option> <option value="appid_desc">App ID (Desc)</option> <option value="foil_last">Foil First</option> <option value="foil_first">Foil Last</option> `; select.value = currentFavoritesSortOrder; select.addEventListener('change', (event) => { currentFavoritesSortOrder = event.target.value; GM_setValue('favoritesSortOrder', currentFavoritesSortOrder); displayFavorites(); }); selectWrapper.appendChild(select); sortGroup.appendChild(selectWrapper); controlsDiv.appendChild(sortGroup); // Rebuild button group const buttonGroup = document.createElement('div'); buttonGroup.className = 'button-group'; const exportButton = document.createElement('button'); exportButton.className = 'steam-button'; exportButton.textContent = 'Export Favorites'; exportButton.onclick = exportFavorites; buttonGroup.appendChild(exportButton); const importButton = document.createElement('button'); importButton.className = 'steam-button'; importButton.textContent = 'Import Favorites'; importButton.onclick = showImportArea; buttonGroup.appendChild(importButton); controlsDiv.appendChild(buttonGroup); logDebug('Favorites controls div rebuilt.'); } else { // If groups exist, just ensure buttons are inside the button-group const buttonGroup = controlsDiv.querySelector('.button-group'); if (buttonGroup) { // Find existing buttons outside the group and move them controlsDiv.querySelectorAll('.steam-button').forEach(btn => { if (!buttonGroup.contains(btn)) { logDebug('Moving existing steam-button into button-group.'); buttonGroup.appendChild(btn); } }); } } } else { logWarn('Favorites controls div not found in existing container.'); } } } // --- Initial Run and Observers --- addStyle(style); // Use a small delay to allow the page structure to load setTimeout(updateBadgeInfo, 500); // Use MutationObserver to detect URL changes within the page // Observe the body for subtree and child list changes to catch SPA navigation let lastUrl = window.location.href; const observer = new MutationObserver(() => { const currentUrl = window.location.href; // Check if the appId part of the URL has changed const lastAppId = lastUrl.match(/\d+$/); const currentAppId = currentUrl.match(/\d+$/); const appIdChanged = (lastAppId && currentAppId && lastAppId[0] !== currentAppId[0]) || (!lastAppId && currentAppId) || (lastAppId && !currentAppId); if (currentUrl !== lastUrl && appIdChanged) { lastUrl = currentUrl; log(`URL changed, App ID updated. Updating badge info.`); // Add a small delay before updating to ensure the page structure is ready setTimeout(updateBadgeInfo, 200); } else if (currentUrl !== lastUrl && !appIdChanged) { // If URL changed but App ID is the same (e.g., hash change for something else), // just re-display favorites in case the container was removed by page script. log(`URL changed, but App ID is the same. Redisplaying favorites.`); lastUrl = currentUrl; // Still update lastUrl // Ensure the favorites container exists before trying to display if (!document.querySelector('.favorites-container')) { logDebug('Favorites container missing during non-AppID URL change, attempting to re-add.'); addFavoritesContainerWithControls(); // Re-add container if necessary } setTimeout(displayFavorites, 100); // Always try to display favorites } else if (currentUrl === lastUrl) { // Check if the favorites container needs to be re-added if (!document.querySelector('.favorites-container')) { logDebug('Favorites container missing, re-adding.'); addFavoritesContainerWithControls(); // Re-add container and controls displayFavorites(); // Populate the re-added container } } }); // Start observing the document body observer.observe(document.body, { subtree: true, childList: true }); // Also try to run updateBadgeInfo on DOMContentLoaded document.addEventListener('DOMContentLoaded', () => { log('DOMContentLoaded event fired.'); // Clear stale API cache on DOMContentLoaded (doesn't need to block anything) clearStaleApiCache(); setTimeout(updateBadgeInfo, 300); }); // Initial display of favorites on load // We need to wait for the DB to be ready before displaying favorites openDatabase().then(() => { logDebug('Database ready for initial favorites display.'); addFavoritesContainerWithControls(); // Ensure container is present early displayFavorites(); }).catch(error => { logError('Failed to open database for initial favorites display:', error); // Still try to add the container and show an error message addFavoritesContainerWithControls(); const favoritesContainer = document.querySelector('.favorites-container'); if (favoritesContainer) { let itemsWrapper = favoritesContainer.querySelector('.favorites-items-wrapper'); if (!itemsWrapper) { itemsWrapper = document.createElement('div'); itemsWrapper.className = 'favorites-items-wrapper'; favoritesContainer.appendChild(itemsWrapper); } itemsWrapper.textContent = 'Error initializing favorites database.'; itemsWrapper.style.color = 'red'; itemsWrapper.style.textAlign = 'center'; itemsWrapper.style.width = '100%'; itemsWrapper.style.marginTop = '10px'; } }); // Clear stale API cache periodically or on load clearStaleApiCache(); })();