|
91 | 91 | class Cache {
|
92 | 92 | constructor() {
|
93 | 93 | this.APP_ID_KEY = "APP_ID";
|
| 94 | + this.PLAYER_COUNT_PREFIX = "PLAYER_COUNT_"; |
94 | 95 | this.cache = {};
|
95 | 96 | this.subscribers = new Map();
|
96 | 97 | this.CACHE_VERSION = "1.0";
|
97 |
| - this.CACHE_EXPIRY = 1000 * 60 * 30; // 30 minutes |
| 98 | + this.CACHE_EXPIRY = 1000 * 60 * 5; // 5 minutes for player count data |
| 99 | + this.CACHE_EXPIRY_LONG = 1000 * 60 * 30; // 30 minutes for other data |
98 | 100 | this.loadFromLocalStorage();
|
99 | 101 | // Clean expired items periodically
|
100 | 102 | setInterval(() => this.cleanExpiredItems(), 1000 * 60 * 5); // Every 5 minutes
|
|
116 | 118 | }
|
117 | 119 | async loadValue(key) {
|
118 | 120 | const cacheItem = this.cache[key];
|
119 |
| - if (cacheItem && this.isValid(cacheItem)) { |
| 121 | + if (cacheItem && this.isValid(cacheItem, this.getExpiryForKey(key))) { |
120 | 122 | return cacheItem.value;
|
121 | 123 | }
|
122 | 124 | // If cache miss or expired, remove it
|
|
138 | 140 | this.notifySubscribers();
|
139 | 141 | }
|
140 | 142 | }
|
141 |
| - isValid(cacheItem) { |
| 143 | + // New method for caching player count data |
| 144 | + async setPlayerCount(appId, count) { |
| 145 | + const key = `${this.PLAYER_COUNT_PREFIX}${appId}`; |
| 146 | + await this.setValue(key, count); |
| 147 | + } |
| 148 | + // New method for retrieving cached player count |
| 149 | + async getPlayerCount(appId) { |
| 150 | + const key = `${this.PLAYER_COUNT_PREFIX}${appId}`; |
| 151 | + return this.loadValue(key); |
| 152 | + } |
| 153 | + getExpiryForKey(key) { |
| 154 | + return key.startsWith(this.PLAYER_COUNT_PREFIX) |
| 155 | + ? this.CACHE_EXPIRY |
| 156 | + : this.CACHE_EXPIRY_LONG; |
| 157 | + } |
| 158 | + isValid(cacheItem, expiry) { |
142 | 159 | if (!cacheItem || !cacheItem.timestamp || !cacheItem.version) {
|
143 | 160 | return false;
|
144 | 161 | }
|
|
148 | 165 | }
|
149 | 166 | // Check expiry
|
150 | 167 | const age = Date.now() - cacheItem.timestamp;
|
151 |
| - return age < this.CACHE_EXPIRY; |
| 168 | + return age < expiry; |
152 | 169 | }
|
153 | 170 | cleanExpiredItems() {
|
154 | 171 | let hasChanges = false;
|
155 | 172 | for (const [key, item] of Object.entries(this.cache)) {
|
156 |
| - if (!this.isValid(item)) { |
| 173 | + if (!this.isValid(item, this.getExpiryForKey(key))) { |
157 | 174 | delete this.cache[key];
|
158 | 175 | hasChanges = true;
|
159 | 176 | }
|
|
189 | 206 | const [appId, setAppId] = React.useState(undefined);
|
190 | 207 | const [playerCount, setPlayerCount] = React.useState("");
|
191 | 208 | const [isVisible, setIsVisible] = React.useState(false);
|
| 209 | + const mountedRef = React.useRef(true); |
192 | 210 | React.useEffect(() => {
|
193 |
| - function loadAppId() { |
194 |
| - CACHE.loadValue(CACHE.APP_ID_KEY).then((id) => { |
195 |
| - console.log("Loaded AppID:", id); // Debug log |
196 |
| - setAppId(id); |
197 |
| - }); |
| 211 | + mountedRef.current = true; |
| 212 | + async function loadAppId() { |
| 213 | + if (!mountedRef.current) |
| 214 | + return; |
| 215 | + const id = await CACHE.loadValue(CACHE.APP_ID_KEY); |
| 216 | + if (!id) { |
| 217 | + setIsVisible(false); |
| 218 | + setAppId(undefined); |
| 219 | + return; |
| 220 | + } |
| 221 | + setAppId(id); |
198 | 222 | }
|
199 | 223 | loadAppId();
|
200 | 224 | CACHE.subscribe("PlayerCount", loadAppId);
|
| 225 | + const handleRouteChange = () => { |
| 226 | + // Check if we're on either a game page or store page |
| 227 | + const isOnGamePage = window.location.pathname.includes('/library/app/'); |
| 228 | + const isOnStorePage = window.location.pathname.includes('/steamweb'); |
| 229 | + if (!isOnGamePage && !isOnStorePage) { |
| 230 | + setIsVisible(false); |
| 231 | + setAppId(undefined); |
| 232 | + CACHE.setValue(CACHE.APP_ID_KEY, ""); // Clear cache when leaving both pages |
| 233 | + } |
| 234 | + }; |
| 235 | + // Listen for both navigation events and history changes |
| 236 | + window.addEventListener('popstate', handleRouteChange); |
| 237 | + window.addEventListener('pushstate', handleRouteChange); |
| 238 | + window.addEventListener('replacestate', handleRouteChange); |
| 239 | + // Initial check |
| 240 | + handleRouteChange(); |
201 | 241 | return () => {
|
| 242 | + mountedRef.current = false; |
202 | 243 | CACHE.unsubscribe("PlayerCount");
|
| 244 | + setIsVisible(false); |
| 245 | + setAppId(undefined); |
| 246 | + setPlayerCount(""); |
| 247 | + window.removeEventListener('popstate', handleRouteChange); |
| 248 | + window.removeEventListener('pushstate', handleRouteChange); |
| 249 | + window.removeEventListener('replacestate', handleRouteChange); |
203 | 250 | };
|
204 | 251 | }, []);
|
205 | 252 | React.useEffect(() => {
|
| 253 | + let interval; |
206 | 254 | const fetchPlayerCount = async () => {
|
207 |
| - if (!appId) { |
| 255 | + if (!appId || !mountedRef.current) { |
208 | 256 | setIsVisible(false);
|
209 | 257 | return;
|
210 | 258 | }
|
211 |
| - console.log("Fetching player count for appId:", appId); // Debug log |
212 | 259 | try {
|
213 |
| - const url = `https://api.steampowered.com/ISteamUserStats/GetNumberOfCurrentPlayers/v1/?appid=${appId}`; |
214 |
| - console.log("Fetching URL:", url); // Debug log |
215 |
| - const response = await serverAPI.fetchNoCors(url, { |
| 260 | + const response = await serverAPI.fetchNoCors(`https://api.steampowered.com/ISteamUserStats/GetNumberOfCurrentPlayers/v1/?appid=${appId}`, { |
216 | 261 | method: "GET",
|
217 |
| - headers: { |
218 |
| - 'Accept': 'application/json' |
219 |
| - } |
| 262 | + headers: { 'Accept': 'application/json' } |
220 | 263 | });
|
221 |
| - console.log("Raw response:", response); // Debug log |
| 264 | + if (!mountedRef.current) |
| 265 | + return; |
222 | 266 | if (response.success) {
|
223 |
| - try { |
224 |
| - const data = JSON.parse(response.result.body); |
225 |
| - console.log("Parsed data:", data); // Debug log |
226 |
| - if (data.response.result === 1) { |
227 |
| - const formattedCount = new Intl.NumberFormat().format(data.response.player_count); |
228 |
| - setPlayerCount(`🟢 Currently Playing: ${formattedCount}`); |
229 |
| - setIsVisible(true); |
230 |
| - } |
231 |
| - else { |
232 |
| - setPlayerCount("No player data available"); |
233 |
| - setIsVisible(true); |
234 |
| - } |
| 267 | + const data = JSON.parse(response.result.body); |
| 268 | + if (data.response.result === 1) { |
| 269 | + const formattedCount = new Intl.NumberFormat().format(data.response.player_count); |
| 270 | + setPlayerCount(`🟢 Currently Playing: ${formattedCount}`); |
| 271 | + setIsVisible(true); |
235 | 272 | }
|
236 |
| - catch (parseError) { |
237 |
| - console.error("Error parsing response:", parseError); |
238 |
| - console.error("Response body:", response.result.body); |
239 |
| - setPlayerCount("Error parsing data"); |
| 273 | + else { |
| 274 | + setPlayerCount("No player data available"); |
240 | 275 | setIsVisible(true);
|
241 | 276 | }
|
242 | 277 | }
|
243 | 278 | else {
|
244 |
| - console.error("Fetch failed:", response); // Debug log |
245 | 279 | throw new Error("Failed to fetch player count");
|
246 | 280 | }
|
247 | 281 | }
|
248 | 282 | catch (error) {
|
249 |
| - console.error("Error in fetchPlayerCount:", error); |
250 |
| - if (error instanceof Error) { |
251 |
| - setPlayerCount(`Error: ${error.message}`); |
252 |
| - } |
253 |
| - else { |
254 |
| - setPlayerCount("Error fetching player count"); |
255 |
| - } |
| 283 | + if (!mountedRef.current) |
| 284 | + return; |
| 285 | + setPlayerCount(error instanceof Error ? `Error: ${error.message}` : "Error fetching player count"); |
256 | 286 | setIsVisible(true);
|
257 | 287 | }
|
258 | 288 | };
|
259 |
| - fetchPlayerCount(); |
| 289 | + if (appId) { |
| 290 | + fetchPlayerCount(); |
| 291 | + interval = setInterval(fetchPlayerCount, 30000); |
| 292 | + } |
| 293 | + else { |
| 294 | + setIsVisible(false); |
| 295 | + } |
| 296 | + return () => { |
| 297 | + if (interval) { |
| 298 | + clearInterval(interval); |
| 299 | + } |
| 300 | + }; |
260 | 301 | }, [appId, serverAPI]);
|
261 |
| - return isVisible ? (window.SP_REACT.createElement("div", { className: deckyFrontendLib.staticClasses.PanelSectionTitle, onClick: () => { |
| 302 | + if (!isVisible) |
| 303 | + return null; |
| 304 | + return (window.SP_REACT.createElement("div", { className: deckyFrontendLib.staticClasses.PanelSectionTitle, onClick: () => { |
262 | 305 | if (appId) {
|
263 | 306 | deckyFrontendLib.Navigation.NavigateToExternalWeb(`https://steamcharts.com/app/${appId}`);
|
264 | 307 | }
|
|
273 | 316 | fontSize: "16px",
|
274 | 317 | zIndex: 7002,
|
275 | 318 | position: "fixed",
|
276 |
| - bottom: 0, |
| 319 | + bottom: 2, |
277 | 320 | left: '50%',
|
278 |
| - transform: `translateX(-50%) translateY(${isVisible ? 0 : 100}%)`, |
279 |
| - transition: "transform 0.22s cubic-bezier(0, 0.73, 0.48, 1)", |
280 |
| - backgroundColor: "#1a1a1a", |
| 321 | + transform: `translateX(-50%)`, |
281 | 322 | color: "#ffffff",
|
282 | 323 | cursor: "pointer",
|
283 |
| - } }, playerCount)) : null; |
| 324 | + } }, playerCount)); |
284 | 325 | };
|
285 | 326 |
|
286 | 327 | const History = deckyFrontendLib.findModuleChild((m) => {
|
|
348 | 389 | };
|
349 | 390 | }
|
350 | 391 |
|
| 392 | + function patchLibrary(serverApi) { |
| 393 | + let isOnLibraryPage = false; |
| 394 | + // Create a reusable patch function |
| 395 | + function patchAppPage(route) { |
| 396 | + const routeProps = deckyFrontendLib.findInReactTree(route, (x) => x?.renderFunc); |
| 397 | + if (routeProps) { |
| 398 | + deckyFrontendLib.afterPatch(routeProps, "renderFunc", (_, ret) => { |
| 399 | + try { |
| 400 | + // Extract appId from URL |
| 401 | + const appId = window.location.pathname.match(/\/library\/app\/([\d]+)/)?.[1]; |
| 402 | + if (appId) { |
| 403 | + isOnLibraryPage = true; |
| 404 | + // Update cache with new appId |
| 405 | + CACHE.setValue(CACHE.APP_ID_KEY, appId); |
| 406 | + } |
| 407 | + } |
| 408 | + catch (error) { |
| 409 | + console.error("Error in library patch:", error); |
| 410 | + } |
| 411 | + return ret; |
| 412 | + }); |
| 413 | + } |
| 414 | + return route; |
| 415 | + } |
| 416 | + const handleRouteChange = () => { |
| 417 | + if (!window.location.pathname.includes('/library/app/')) { |
| 418 | + if (isOnLibraryPage) { |
| 419 | + isOnLibraryPage = false; |
| 420 | + CACHE.setValue(CACHE.APP_ID_KEY, ""); |
| 421 | + } |
| 422 | + } |
| 423 | + }; |
| 424 | + window.addEventListener('popstate', handleRouteChange); |
| 425 | + // Add the library page patch |
| 426 | + const unpatch = serverApi.routerHook.addPatch('/library/app/:appid', patchAppPage); |
| 427 | + // Return cleanup function |
| 428 | + return () => { |
| 429 | + if (unpatch) { |
| 430 | + unpatch(null); |
| 431 | + } |
| 432 | + window.removeEventListener('popstate', handleRouteChange); |
| 433 | + if (isOnLibraryPage) { |
| 434 | + CACHE.setValue(CACHE.APP_ID_KEY, ""); |
| 435 | + } |
| 436 | + }; |
| 437 | + } |
| 438 | + |
351 | 439 | var index = deckyFrontendLib.definePlugin((serverApi) => {
|
352 | 440 | Cache.init();
|
| 441 | + // Add global component |
353 | 442 | serverApi.routerHook.addGlobalComponent("PlayerCount", () => window.SP_REACT.createElement(PlayerCount, { serverAPI: serverApi }));
|
| 443 | + // Initialize patches |
354 | 444 | const storePatch = patchStore(serverApi);
|
| 445 | + const libraryPatch = patchLibrary(serverApi); |
355 | 446 | return {
|
356 | 447 | title: window.SP_REACT.createElement("div", { className: deckyFrontendLib.staticClasses.Title }, "Player Pulse"),
|
357 | 448 | content: (window.SP_REACT.createElement(deckyFrontendLib.PanelSection, { title: "About" },
|
|
374 | 465 | icon: window.SP_REACT.createElement(FaUsers, null),
|
375 | 466 | onDismount() {
|
376 | 467 | serverApi.routerHook.removeGlobalComponent("PlayerCount");
|
377 |
| - storePatch?.(); |
| 468 | + if (storePatch) |
| 469 | + storePatch(); |
| 470 | + if (libraryPatch) |
| 471 | + libraryPatch(); |
378 | 472 | },
|
379 | 473 | };
|
380 | 474 | });
|
|
0 commit comments