1window.SteamWidget = (function() { 2 'use strict'; 3 4 function getCache(key) { 5 if (window.ApiRateLimiter) { 6 return window.ApiRateLimiter.getCache(key); 7 } 8 try { 9 var data = localStorage.getItem(key); 10 if (!data) return null; 11 var parsed = JSON.parse(data); 12 if (parsed && parsed.value && Object.keys(parsed.value).length > 0) { 13 if (Date.now() - parsed.timestamp < (60 * 60 * 1000)) { 14 return parsed.value; 15 } 16 } 17 } catch(e) {} 18 return null; 19 } 20 21 function setCache(key, value) { 22 if (window.ApiRateLimiter) { 23 window.ApiRateLimiter.setCache(key, value); 24 return; 25 } 26 try { 27 if (!value || Object.keys(value).length === 0) return; 28 localStorage.setItem(key, JSON.stringify({ 29 timestamp: Date.now(), 30 value: value 31 })); 32 } catch(e) {} 33 } 34 35 function clearCache(key) { 36 if (window.ApiRateLimiter) { 37 window.ApiRateLimiter.clearCache(key); 38 return; 39 } 40 try { 41 localStorage.removeItem(key); 42 } catch(e) {} 43 } 44 45 function fetchJson(url) { 46 return fetch(url, { 47 method: 'GET', 48 mode: 'cors', 49 credentials: 'omit', 50 headers: { 51 'Accept': 'application/json, text/plain, */*' 52 } 53 }).then(function(res) { 54 if (!res.ok) throw new Error('HTTP ' + res.status); 55 return res.json(); 56 }); 57 } 58 59 function renderLoading(element) { 60 if (!element) return; 61 var container = element.querySelector('.project-live-stats'); 62 if (!container) { 63 container = document.createElement('span'); 64 container.className = 'project-live-stats steam-live-stats'; 65 element.appendChild(container); 66 } 67 container.innerHTML = '<span class="stat-chip stat-loading" title="Loading live stats..."><span class="stat-icon" style="-webkit-mask-image: url(\'/icons/hourglass.svg\'); mask-image: url(\'/icons/hourglass.svg\');"></span></span>'; 68 } 69 70 function renderError(element, onRetry) { 71 if (!element) return; 72 var container = element.querySelector('.project-live-stats'); 73 if (!container) { 74 container = document.createElement('span'); 75 container.className = 'project-live-stats steam-live-stats'; 76 element.appendChild(container); 77 } 78 container.innerHTML = '<span class="stat-chip stat-error" title="Data Couldn\'t Load (Right-click to retry)"><span class="stat-icon" style="-webkit-mask-image: url(\'/icons/xmark.svg\'); mask-image: url(\'/icons/xmark.svg\');"></span></span>'; 79 80 var errChip = container.querySelector('.stat-error'); 81 if (errChip && typeof onRetry === 'function') { 82 errChip.addEventListener('contextmenu', function(e) { 83 e.preventDefault(); 84 e.stopPropagation(); 85 onRetry(); 86 }); 87 } 88 } 89 90 function render(element, stats) { 91 if (!element || !stats || Object.keys(stats).length === 0) return; 92 var container = element.querySelector('.project-live-stats'); 93 if (!container) { 94 container = document.createElement('span'); 95 container.className = 'project-live-stats steam-live-stats'; 96 element.appendChild(container); 97 } 98 99 function makeIcon(file) { 100 return '<span class="stat-icon" style="-webkit-mask-image: url(\'/icons/' + file + '\'); mask-image: url(\'/icons/' + file + '\');"></span>'; 101 } 102 103 var isWhoamiContainer = element.closest && element.closest('.whoami-widget-container'); 104 var layout = isWhoamiContainer ? 'badge' : (element.getAttribute('data-desktop-layout') || element.getAttribute('data-widget-layout') || 'badge'); 105 element.setAttribute('data-widget-layout', layout); 106 107 if (layout === 'square' || layout === 'rectangle') { 108 element.classList.add(layout === 'square' ? 'widget-card-square' : 'widget-card-rectangle'); 109 var bodyChips = ''; 110 if (stats.price) { 111 bodyChips += '<span class="stat-chip stat-price" title="Price">' + makeIcon('tag.svg') + ' ' + stats.price + '</span>'; 112 } 113 if (stats.review_score) { 114 var sLowerRev = String(stats.review_score).toLowerCase(); 115 var rIcon = makeIcon('star.svg'); 116 if (sLowerRev.indexOf('positive') !== -1) rIcon = makeIcon('thumbs-up.svg'); 117 else if (sLowerRev.indexOf('mixed') !== -1) rIcon = makeIcon('face-smile.svg'); 118 else if (sLowerRev.indexOf('negative') !== -1) rIcon = makeIcon('thumbs-down.svg'); 119 else if (sLowerRev.indexOf('no user reviews') !== -1) rIcon = makeIcon('comment.svg'); 120 121 var rLabel = stats.review_score; 122 if (stats.review_count) rLabel += ' (' + stats.review_count + ')'; 123 124 bodyChips += '<span class="stat-chip stat-review" title="' + rLabel.replace(/"/g, '"') + '">' + rIcon + ' ' + rLabel + '</span>'; 125 } 126 127 var statusChip = ''; 128 var sLower = (stats.status || '').toLowerCase(); 129 if (sLower.indexOf('demo') !== -1) { 130 statusChip = '<span class="stat-chip stat-status">' + makeIcon('film.svg') + ' Demo</span>'; 131 } else if (sLower.indexOf('soon') !== -1) { 132 statusChip = '<span class="stat-chip stat-status">' + makeIcon('calendar-day.svg') + ' Soon</span>'; 133 } else if (stats.status) { 134 statusChip = '<span class="stat-chip stat-status">' + makeIcon('circle-check.svg') + ' ' + stats.status + '</span>'; 135 } else { 136 statusChip = '<span class="stat-chip stat-status">' + makeIcon('circle-check.svg') + ' Available</span>'; 137 } 138 139 var imgUrl = element.getAttribute('data-widget-image'); 140 var imgSide = element.getAttribute('data-widget-image-side') || 'left'; 141 142 var cardContentHtml = '<div class="widget-card-header">' + 143 '<span class="widget-card-title">' + makeIcon('steam-symbol.svg') + ' STEAM</span>' + 144 statusChip + 145 '</div>' + 146 '<div class="widget-card-body">' + 147 (bodyChips || '<span class="stat-chip stat-price">' + makeIcon('tag.svg') + ' Available</span>') + 148 '</div>' + 149 '<div class="widget-card-footer">' + 150 '<span class="export-btn">[ STORE PAGE â ]</span>' + 151 '</div>'; 152 153 if (layout === 'rectangle' && imgUrl) { 154 var sideClass = imgSide === 'right' ? 'img-right' : 'img-left'; 155 var imgHtml = '<div class="widget-card-image-col ' + sideClass + '"><img src="' + imgUrl + '" class="widget-card-image" alt="Game Thumbnail" /></div>'; 156 var contentColHtml = '<div class="widget-card-content-col">' + cardContentHtml + '</div>'; 157 element.innerHTML = '<div class="widget-card-split">' + imgHtml + contentColHtml + '</div>'; 158 } else { 159 element.innerHTML = cardContentHtml; 160 } 161 return; 162 } 163 164 element.classList.remove('widget-card-square', 'widget-card-rectangle'); 165 var html = ''; 166 167 // 1. Release Status (Full / Demo / Soon) 168 if (stats.status) { 169 var sIcon = ''; 170 if (stats.status === 'Full') { 171 sIcon = makeIcon('circle-check.svg'); 172 } else if (stats.status === 'Demo') { 173 sIcon = makeIcon('flask.svg'); 174 } else if (stats.status === 'Soon') { 175 sIcon = makeIcon('clock.svg'); 176 } 177 html += '<span class="stat-chip stat-status" title="Release Status">' + (sIcon ? sIcon + ' ' : '') + stats.status + '</span>'; 178 } 179 180 // 2. Price / Free 181 if (stats.price) { 182 html += '<span class="stat-chip stat-price" title="Price">' + makeIcon('tag.svg') + ' ' + stats.price + '</span>'; 183 } 184 185 // 3. Reviews & Review Score Expressive Rating Icon 186 if (stats.review_score) { 187 var rText = ''; 188 var rTitle = stats.review_score; 189 if (stats.review_count) rTitle += ' (' + stats.review_count + ' reviews)'; 190 191 var sLower = String(stats.review_score).toLowerCase(); 192 if (sLower.indexOf('no user reviews') !== -1 || stats.review_count === '0' || stats.review_count === 0) { 193 rText = makeIcon('comment.svg') + ' No reviews'; 194 } else if (sLower.indexOf('positive') !== -1) { 195 rText = makeIcon('thumbs-up.svg') + (stats.review_count ? ' (' + stats.review_count + ')' : ' +'); 196 } else if (sLower.indexOf('mixed') !== -1) { 197 rText = makeIcon('face-smile.svg') + (stats.review_count ? ' (' + stats.review_count + ')' : ' ~'); 198 } else if (sLower.indexOf('negative') !== -1) { 199 rText = makeIcon('thumbs-down.svg') + (stats.review_count ? ' (' + stats.review_count + ')' : ' -'); 200 } else { 201 rText = makeIcon('star.svg') + ' ' + stats.review_score; 202 } 203 html += '<span class="stat-chip stat-review-score" title="' + rTitle.replace(/"/g, '"') + '">' + rText + '</span>'; 204 } 205 206 // 4. Wishlists / Followers 207 if (stats.wishlists || stats.followers) { 208 var wCount = stats.wishlists || stats.followers; 209 html += '<span class="stat-chip stat-wishlist" title="Wishlists / Followers">' + makeIcon('heart.svg') + ' ' + wCount + ' wishlists</span>'; 210 } 211 212 container.innerHTML = html; 213 } 214 215 function fetchStatsForApp(appid, callback, errorCallback) { 216 var baseUrl = (window.ApiRateLimiter && window.ApiRateLimiter.getApiBaseUrl) ? window.ApiRateLimiter.getApiBaseUrl() : ''; 217 var proxyUrl = baseUrl + '/api/proxy/steam?appid=' + appid; 218 219 var taskFn = function() { 220 return fetchJson(proxyUrl).then(function(stats) { 221 if (!stats || Object.keys(stats).length === 0) { 222 throw new Error("Empty Steam stats payload"); 223 } 224 return stats; 225 }); 226 }; 227 228 if (window.ApiRateLimiter) { 229 window.ApiRateLimiter.enqueue(proxyUrl, taskFn, callback, errorCallback); 230 } else { 231 taskFn().then(callback).catch(errorCallback); 232 } 233 } 234 235 function initElement(el) { 236 var appid = el.getAttribute('data-steam-appid'); 237 if (!appid) return; 238 239 var initialStats = {}; 240 var statusAttr = el.getAttribute('data-steam-status'); 241 var priceAttr = el.getAttribute('data-steam-price'); 242 var reviewScoreAttr = el.getAttribute('data-steam-review-score'); 243 var reviewCountAttr = el.getAttribute('data-steam-review-count'); 244 245 if (statusAttr) initialStats.status = statusAttr; 246 if (priceAttr) initialStats.price = priceAttr; 247 if (reviewScoreAttr) initialStats.review_score = reviewScoreAttr; 248 if (reviewCountAttr) initialStats.review_count = reviewCountAttr; 249 250 if (Object.keys(initialStats).length > 0) { 251 render(el, initialStats); 252 } 253 254 var cacheKey = 'solvin_steam_stats_v8_' + appid; 255 var cached = getCache(cacheKey); 256 if (cached && Object.keys(cached).length > 0) { 257 render(el, cached); 258 return; 259 } 260 261 // Show hourglass loading spinner while pending 262 if (Object.keys(initialStats).length === 0) { 263 renderLoading(el); 264 } 265 266 var baseUrl = (window.ApiRateLimiter && window.ApiRateLimiter.getApiBaseUrl) ? window.ApiRateLimiter.getApiBaseUrl() : ''; 267 var proxyUrl = baseUrl + '/api/proxy/steam?appid=' + appid; 268 269 function handleRetry() { 270 clearCache(cacheKey); 271 if (window.ApiRateLimiter) { 272 window.ApiRateLimiter.resetDomain(proxyUrl); 273 } 274 275 renderLoading(el); 276 fetchStatsForApp(appid, function(stats) { 277 if (stats && Object.keys(stats).length > 0) { 278 setCache(cacheKey, stats); 279 render(el, stats); 280 } else { 281 renderError(el, handleRetry); 282 } 283 }, function() { 284 renderError(el, handleRetry); 285 }); 286 } 287 288 fetchStatsForApp(appid, function(stats) { 289 if (stats && Object.keys(stats).length > 0) { 290 setCache(cacheKey, stats); 291 render(el, stats); 292 } else { 293 renderError(el, handleRetry); 294 } 295 }, function() { 296 renderError(el, handleRetry); 297 }); 298 } 299
300 function init() { 301 var steamElements = document.querySelectorAll('[data-steam-appid]'); 302 steamElements.forEach(function(el) { 303 if (el.classList.contains('fav-collection-item-chip') || (el.closest && el.closest('.fav-collection-item-chip'))) return; 304 initElement(el); 305 }); 306 } 307 308 return { init: init, initElement: initElement }; 309})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.