Skip to content

Commit 0a5719f

Browse files
committedDec 29, 2024
Game Library PlayerCount
1 parent 540fa09 commit 0a5719f

13 files changed

+319
-123
lines changed
 

‎.DS_Store

0 Bytes
Binary file not shown.

‎README.md

+3-9
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,21 @@
22

33
<img src="/assets/playcount-logo.png" alt="PlayCount Banner"/>
44

5-
[![Decky Downloads](https://img.shields.io/github/downloads/itsOwen/playcount/total?color=455da5&style=for-the-badge&label=Downloads)](https://github.com/itsOwen/playcount-decky/releases/latest)
6-
[![Latest Release](https://img.shields.io/github/v/release/itsOwen/playcount?color=455da5&style=for-the-badge&label=Latest)](https://github.com/itsOwen/playcount-decky/releases/latest)
7-
85
A powerful Steam Deck plugin that shows real-time player counts for your Steam games! Stay informed about the active player base of any game in your library and also while purchasing games on Steam Store 👥
96

107
## ✨ Features
118

129
- Clean and minimal interface
1310
- Display Live Player Count in Steam Store.
14-
- Display Live Player Count in Steam Library Games (Upcoming)
11+
- Display Live Player Count in Steam Library Games.
1512

1613
## 📸 Screenshots
1714

1815
### Game Library View
19-
<img src="/assets/PlayCount.jpg" alt="Count1"/>
16+
<img src="/assets/PlayCount1.jpg" alt="Count1"/>
2017

2118
### Store Page Integration
22-
<img src="/assets/PlayCount2.jpg" alt="Count2"/>
23-
24-
### Quick Access Menu
25-
<img src="/assets/PlayCount3.jpg" alt="Count3"/>
19+
<img src="/assets/PlayCount.jpg" alt="Count2"/>
2620

2721
## 🚀 Installation
2822

‎assets/PlayCount.jpg

-16.3 KB
Loading

‎assets/PlayCount1.jpg

144 KB
Loading

‎assets/PlayCount2.jpg

-193 KB
Binary file not shown.

‎assets/PlayCount3.jpg

-186 KB
Binary file not shown.

‎dist/index.js

+145-51
Original file line numberDiff line numberDiff line change
@@ -91,10 +91,12 @@
9191
class Cache {
9292
constructor() {
9393
this.APP_ID_KEY = "APP_ID";
94+
this.PLAYER_COUNT_PREFIX = "PLAYER_COUNT_";
9495
this.cache = {};
9596
this.subscribers = new Map();
9697
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
98100
this.loadFromLocalStorage();
99101
// Clean expired items periodically
100102
setInterval(() => this.cleanExpiredItems(), 1000 * 60 * 5); // Every 5 minutes
@@ -116,7 +118,7 @@
116118
}
117119
async loadValue(key) {
118120
const cacheItem = this.cache[key];
119-
if (cacheItem && this.isValid(cacheItem)) {
121+
if (cacheItem && this.isValid(cacheItem, this.getExpiryForKey(key))) {
120122
return cacheItem.value;
121123
}
122124
// If cache miss or expired, remove it
@@ -138,7 +140,22 @@
138140
this.notifySubscribers();
139141
}
140142
}
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) {
142159
if (!cacheItem || !cacheItem.timestamp || !cacheItem.version) {
143160
return false;
144161
}
@@ -148,12 +165,12 @@
148165
}
149166
// Check expiry
150167
const age = Date.now() - cacheItem.timestamp;
151-
return age < this.CACHE_EXPIRY;
168+
return age < expiry;
152169
}
153170
cleanExpiredItems() {
154171
let hasChanges = false;
155172
for (const [key, item] of Object.entries(this.cache)) {
156-
if (!this.isValid(item)) {
173+
if (!this.isValid(item, this.getExpiryForKey(key))) {
157174
delete this.cache[key];
158175
hasChanges = true;
159176
}
@@ -189,76 +206,102 @@
189206
const [appId, setAppId] = React.useState(undefined);
190207
const [playerCount, setPlayerCount] = React.useState("");
191208
const [isVisible, setIsVisible] = React.useState(false);
209+
const mountedRef = React.useRef(true);
192210
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);
198222
}
199223
loadAppId();
200224
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();
201241
return () => {
242+
mountedRef.current = false;
202243
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);
203250
};
204251
}, []);
205252
React.useEffect(() => {
253+
let interval;
206254
const fetchPlayerCount = async () => {
207-
if (!appId) {
255+
if (!appId || !mountedRef.current) {
208256
setIsVisible(false);
209257
return;
210258
}
211-
console.log("Fetching player count for appId:", appId); // Debug log
212259
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}`, {
216261
method: "GET",
217-
headers: {
218-
'Accept': 'application/json'
219-
}
262+
headers: { 'Accept': 'application/json' }
220263
});
221-
console.log("Raw response:", response); // Debug log
264+
if (!mountedRef.current)
265+
return;
222266
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);
235272
}
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");
240275
setIsVisible(true);
241276
}
242277
}
243278
else {
244-
console.error("Fetch failed:", response); // Debug log
245279
throw new Error("Failed to fetch player count");
246280
}
247281
}
248282
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");
256286
setIsVisible(true);
257287
}
258288
};
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+
};
260301
}, [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: () => {
262305
if (appId) {
263306
deckyFrontendLib.Navigation.NavigateToExternalWeb(`https://steamcharts.com/app/${appId}`);
264307
}
@@ -273,14 +316,12 @@
273316
fontSize: "16px",
274317
zIndex: 7002,
275318
position: "fixed",
276-
bottom: 0,
319+
bottom: 2,
277320
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%)`,
281322
color: "#ffffff",
282323
cursor: "pointer",
283-
} }, playerCount)) : null;
324+
} }, playerCount));
284325
};
285326

286327
const History = deckyFrontendLib.findModuleChild((m) => {
@@ -348,10 +389,60 @@
348389
};
349390
}
350391

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+
351439
var index = deckyFrontendLib.definePlugin((serverApi) => {
352440
Cache.init();
441+
// Add global component
353442
serverApi.routerHook.addGlobalComponent("PlayerCount", () => window.SP_REACT.createElement(PlayerCount, { serverAPI: serverApi }));
443+
// Initialize patches
354444
const storePatch = patchStore(serverApi);
445+
const libraryPatch = patchLibrary(serverApi);
355446
return {
356447
title: window.SP_REACT.createElement("div", { className: deckyFrontendLib.staticClasses.Title }, "Player Pulse"),
357448
content: (window.SP_REACT.createElement(deckyFrontendLib.PanelSection, { title: "About" },
@@ -374,7 +465,10 @@
374465
icon: window.SP_REACT.createElement(FaUsers, null),
375466
onDismount() {
376467
serverApi.routerHook.removeGlobalComponent("PlayerCount");
377-
storePatch?.();
468+
if (storePatch)
469+
storePatch();
470+
if (libraryPatch)
471+
libraryPatch();
378472
},
379473
};
380474
});

‎package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "playcount-decky",
3-
"version": "1.0",
3+
"version": "1.1",
44
"description": "A Steam Deck plugin that shows current player counts for your steam games.",
55
"scripts": {
66
"build": "shx rm -rf dist && rollup -c",

‎src/components/PlayerCount.tsx

+76-51
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import { FC } from 'react';
1+
import { FC, useEffect, useState, useRef } from 'react';
22
import { Navigation, staticClasses, ServerAPI } from 'decky-frontend-lib';
3-
import { useEffect, useState } from 'react';
43
import { CACHE } from '../utils/Cache';
54

65
interface PlayerCountProps {
@@ -18,92 +17,120 @@ export const PlayerCount: FC<PlayerCountProps> = ({ serverAPI }) => {
1817
const [appId, setAppId] = useState<string | undefined>(undefined);
1918
const [playerCount, setPlayerCount] = useState<string>("");
2019
const [isVisible, setIsVisible] = useState<boolean>(false);
20+
const mountedRef = useRef(true);
2121

2222
useEffect(() => {
23-
function loadAppId() {
24-
CACHE.loadValue(CACHE.APP_ID_KEY).then((id) => {
25-
console.log("Loaded AppID:", id); // Debug log
26-
setAppId(id);
27-
});
23+
mountedRef.current = true;
24+
25+
async function loadAppId() {
26+
if (!mountedRef.current) return;
27+
const id = await CACHE.loadValue(CACHE.APP_ID_KEY);
28+
if (!id) {
29+
setIsVisible(false);
30+
setAppId(undefined);
31+
return;
32+
}
33+
setAppId(id);
2834
}
35+
2936
loadAppId();
3037
CACHE.subscribe("PlayerCount", loadAppId);
3138

39+
const handleRouteChange = () => {
40+
// Check if we're on either a game page or store page
41+
const isOnGamePage = window.location.pathname.includes('/library/app/');
42+
const isOnStorePage = window.location.pathname.includes('/steamweb');
43+
44+
if (!isOnGamePage && !isOnStorePage) {
45+
setIsVisible(false);
46+
setAppId(undefined);
47+
CACHE.setValue(CACHE.APP_ID_KEY, ""); // Clear cache when leaving both pages
48+
}
49+
};
50+
51+
// Listen for both navigation events and history changes
52+
window.addEventListener('popstate', handleRouteChange);
53+
window.addEventListener('pushstate', handleRouteChange);
54+
window.addEventListener('replacestate', handleRouteChange);
55+
56+
// Initial check
57+
handleRouteChange();
58+
3259
return () => {
60+
mountedRef.current = false;
3361
CACHE.unsubscribe("PlayerCount");
62+
setIsVisible(false);
63+
setAppId(undefined);
64+
setPlayerCount("");
65+
window.removeEventListener('popstate', handleRouteChange);
66+
window.removeEventListener('pushstate', handleRouteChange);
67+
window.removeEventListener('replacestate', handleRouteChange);
3468
};
3569
}, []);
3670

3771
useEffect(() => {
72+
let interval: ReturnType<typeof setInterval> | undefined;
73+
3874
const fetchPlayerCount = async () => {
39-
if (!appId) {
75+
if (!appId || !mountedRef.current) {
4076
setIsVisible(false);
4177
return;
4278
}
4379

44-
console.log("Fetching player count for appId:", appId); // Debug log
45-
4680
try {
47-
const url = `https://api.steampowered.com/ISteamUserStats/GetNumberOfCurrentPlayers/v1/?appid=${appId}`;
48-
console.log("Fetching URL:", url); // Debug log
49-
5081
const response = await serverAPI.fetchNoCors<{ body: string }>(
51-
url,
82+
`https://api.steampowered.com/ISteamUserStats/GetNumberOfCurrentPlayers/v1/?appid=${appId}`,
5283
{
5384
method: "GET",
54-
headers: {
55-
'Accept': 'application/json'
56-
}
85+
headers: { 'Accept': 'application/json' }
5786
}
5887
);
5988

60-
console.log("Raw response:", response); // Debug log
89+
if (!mountedRef.current) return;
6190

6291
if (response.success) {
63-
try {
64-
const data: SteamPlayerResponse = JSON.parse(response.result.body);
65-
console.log("Parsed data:", data); // Debug log
66-
67-
if (data.response.result === 1) {
68-
const formattedCount = new Intl.NumberFormat().format(data.response.player_count);
69-
setPlayerCount(`🟢 Currently Playing: ${formattedCount}`);
70-
setIsVisible(true);
71-
} else {
72-
setPlayerCount("No player data available");
73-
setIsVisible(true);
74-
}
75-
} catch (parseError) {
76-
console.error("Error parsing response:", parseError);
77-
console.error("Response body:", response.result.body);
78-
setPlayerCount("Error parsing data");
92+
const data: SteamPlayerResponse = JSON.parse(response.result.body);
93+
94+
if (data.response.result === 1) {
95+
const formattedCount = new Intl.NumberFormat().format(data.response.player_count);
96+
setPlayerCount(`🟢 Currently Playing: ${formattedCount}`);
97+
setIsVisible(true);
98+
} else {
99+
setPlayerCount("No player data available");
79100
setIsVisible(true);
80101
}
81102
} else {
82-
console.error("Fetch failed:", response); // Debug log
83103
throw new Error("Failed to fetch player count");
84104
}
85105
} catch (error) {
86-
console.error("Error in fetchPlayerCount:", error);
87-
if (error instanceof Error) {
88-
setPlayerCount(`Error: ${error.message}`);
89-
} else {
90-
setPlayerCount("Error fetching player count");
91-
}
106+
if (!mountedRef.current) return;
107+
setPlayerCount(error instanceof Error ? `Error: ${error.message}` : "Error fetching player count");
92108
setIsVisible(true);
93109
}
94110
};
95111

96-
fetchPlayerCount();
112+
if (appId) {
113+
fetchPlayerCount();
114+
interval = setInterval(fetchPlayerCount, 30000);
115+
} else {
116+
setIsVisible(false);
117+
}
118+
119+
return () => {
120+
if (interval) {
121+
clearInterval(interval);
122+
}
123+
};
97124
}, [appId, serverAPI]);
98125

99-
return isVisible ? (
126+
if (!isVisible) return null;
127+
128+
return (
100129
<div
101130
className={staticClasses.PanelSectionTitle}
102131
onClick={() => {
103132
if (appId) {
104-
Navigation.NavigateToExternalWeb(
105-
`https://steamcharts.com/app/${appId}`
106-
);
133+
Navigation.NavigateToExternalWeb(`https://steamcharts.com/app/${appId}`);
107134
}
108135
}}
109136
style={{
@@ -117,16 +144,14 @@ export const PlayerCount: FC<PlayerCountProps> = ({ serverAPI }) => {
117144
fontSize: "16px",
118145
zIndex: 7002,
119146
position: "fixed",
120-
bottom: 0,
147+
bottom: 2,
121148
left: '50%',
122-
transform: `translateX(-50%) translateY(${isVisible ? 0 : 100}%)`,
123-
transition: "transform 0.22s cubic-bezier(0, 0.73, 0.48, 1)",
124-
backgroundColor: "#1a1a1a",
149+
transform: `translateX(-50%)`,
125150
color: "#ffffff",
126151
cursor: "pointer",
127152
}}
128153
>
129154
{playerCount}
130155
</div>
131-
) : null;
156+
);
132157
};

‎src/index.tsx

+7-2
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,21 @@ import {
99
import { FaUsers, FaGithub, FaTwitter, FaInstagram } from "react-icons/fa";
1010
import { PlayerCount } from "./components/PlayerCount";
1111
import { patchStore } from "./patches/StorePatch";
12+
import { patchLibrary } from "./patches/LibraryPatch";
1213
import { Cache } from "./utils/Cache";
1314

1415
export default definePlugin((serverApi: ServerAPI) => {
1516
Cache.init();
1617

18+
// Add global component
1719
serverApi.routerHook.addGlobalComponent(
1820
"PlayerCount",
1921
() => <PlayerCount serverAPI={serverApi} />
2022
);
2123

24+
// Initialize patches
2225
const storePatch = patchStore(serverApi);
26+
const libraryPatch = patchLibrary(serverApi);
2327

2428
return {
2529
title: <div className={staticClasses.Title}>Player Pulse</div>,
@@ -63,7 +67,8 @@ export default definePlugin((serverApi: ServerAPI) => {
6367
icon: <FaUsers />,
6468
onDismount() {
6569
serverApi.routerHook.removeGlobalComponent("PlayerCount");
66-
storePatch?.();
70+
if (storePatch) storePatch();
71+
if (libraryPatch) libraryPatch();
6772
},
6873
};
69-
});
74+
});

‎src/patches/LibraryPatch.ts

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import {
2+
afterPatch,
3+
findInReactTree,
4+
ServerAPI,
5+
} from 'decky-frontend-lib';
6+
import { CACHE } from "../utils/Cache";
7+
import { ReactElement } from 'react';
8+
9+
export function patchLibrary(serverApi: ServerAPI): () => void {
10+
let isOnLibraryPage = false;
11+
12+
// Create a reusable patch function
13+
function patchAppPage(route: any) {
14+
const routeProps = findInReactTree(route, (x: any) => x?.renderFunc);
15+
16+
if (routeProps) {
17+
afterPatch(routeProps, "renderFunc", (_: any, ret: ReactElement) => {
18+
try {
19+
// Extract appId from URL
20+
const appId = window.location.pathname.match(/\/library\/app\/([\d]+)/)?.[1];
21+
22+
if (appId) {
23+
isOnLibraryPage = true;
24+
// Update cache with new appId
25+
CACHE.setValue(CACHE.APP_ID_KEY, appId);
26+
}
27+
} catch (error) {
28+
console.error("Error in library patch:", error);
29+
}
30+
31+
return ret;
32+
});
33+
}
34+
return route;
35+
}
36+
37+
const handleRouteChange = () => {
38+
if (!window.location.pathname.includes('/library/app/')) {
39+
if (isOnLibraryPage) {
40+
isOnLibraryPage = false;
41+
CACHE.setValue(CACHE.APP_ID_KEY, "");
42+
}
43+
}
44+
};
45+
46+
window.addEventListener('popstate', handleRouteChange);
47+
48+
// Add the library page patch
49+
const unpatch = serverApi.routerHook.addPatch('/library/app/:appid', patchAppPage);
50+
51+
// Return cleanup function
52+
return () => {
53+
if (unpatch) {
54+
unpatch(null as any);
55+
}
56+
window.removeEventListener('popstate', handleRouteChange);
57+
if (isOnLibraryPage) {
58+
CACHE.setValue(CACHE.APP_ID_KEY, "");
59+
}
60+
};
61+
}

‎src/patches/StorePatch.ts

+1-4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import { ServerAPI, findModuleChild } from "decky-frontend-lib"
22
import { CACHE } from "../utils/Cache"
33

4-
// most of the below is stolen from https://github.com/OMGDuke/protondb-decky/tree/28/store-injection
5-
64
type Tab = {
75
description: string
86
devtoolsFrontendUrl: string
@@ -92,5 +90,4 @@ export function patchStore(serverApi: ServerAPI): () => void {
9290
return () => {
9391
CACHE.setValue(CACHE.APP_ID_KEY, "");
9492
};
95-
}
96-
93+
}

‎src/utils/Cache.ts

+25-5
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ export let CACHE: Cache;
22

33
export class Cache {
44
public readonly APP_ID_KEY = "APP_ID";
5+
public readonly PLAYER_COUNT_PREFIX = "PLAYER_COUNT_";
56
private cache: Partial<Record<string, any>> = {};
67
private subscribers: Map<string, () => void> = new Map();
78
private readonly CACHE_VERSION = "1.0";
8-
private readonly CACHE_EXPIRY = 1000 * 60 * 30; // 30 minutes
9+
private readonly CACHE_EXPIRY = 1000 * 60 * 5; // 5 minutes for player count data
10+
private readonly CACHE_EXPIRY_LONG = 1000 * 60 * 30; // 30 minutes for other data
911

1012
constructor() {
1113
this.loadFromLocalStorage();
@@ -35,7 +37,7 @@ export class Cache {
3537
async loadValue(key: string): Promise<any> {
3638
const cacheItem = this.cache[key];
3739

38-
if (cacheItem && this.isValid(cacheItem)) {
40+
if (cacheItem && this.isValid(cacheItem, this.getExpiryForKey(key))) {
3941
return cacheItem.value;
4042
}
4143

@@ -61,7 +63,25 @@ export class Cache {
6163
}
6264
}
6365

64-
private isValid(cacheItem: any): boolean {
66+
// New method for caching player count data
67+
async setPlayerCount(appId: string, count: number): Promise<void> {
68+
const key = `${this.PLAYER_COUNT_PREFIX}${appId}`;
69+
await this.setValue(key, count);
70+
}
71+
72+
// New method for retrieving cached player count
73+
async getPlayerCount(appId: string): Promise<number | null> {
74+
const key = `${this.PLAYER_COUNT_PREFIX}${appId}`;
75+
return this.loadValue(key);
76+
}
77+
78+
private getExpiryForKey(key: string): number {
79+
return key.startsWith(this.PLAYER_COUNT_PREFIX)
80+
? this.CACHE_EXPIRY
81+
: this.CACHE_EXPIRY_LONG;
82+
}
83+
84+
private isValid(cacheItem: any, expiry: number): boolean {
6585
if (!cacheItem || !cacheItem.timestamp || !cacheItem.version) {
6686
return false;
6787
}
@@ -73,13 +93,13 @@ export class Cache {
7393

7494
// Check expiry
7595
const age = Date.now() - cacheItem.timestamp;
76-
return age < this.CACHE_EXPIRY;
96+
return age < expiry;
7797
}
7898

7999
private cleanExpiredItems(): void {
80100
let hasChanges = false;
81101
for (const [key, item] of Object.entries(this.cache)) {
82-
if (!this.isValid(item)) {
102+
if (!this.isValid(item, this.getExpiryForKey(key))) {
83103
delete this.cache[key];
84104
hasChanges = true;
85105
}

0 commit comments

Comments
 (0)
Please sign in to comment.