PageSourceSearch

https://sol.vin/js/widgets/steam_widget.js

js sol.vin collected 2026-10-09 21:58:34 UTC 12,101 bytes, 309 lines download raw bytes

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, '&quot;') + '">' + 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, '&quot;') + '">' + 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.