Skip to content

Commit 8a856a6

Browse files
committed
feat: storage manager
allows you delete anything stored by extendium
1 parent 9be2846 commit 8a856a6

File tree

10 files changed

+230
-28
lines changed

10 files changed

+230
-28
lines changed

devtools.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import CDP from 'chrome-remote-interface';
2+
3+
async function main(): Promise<void> {
4+
const host = '127.0.0.1';
5+
const port = 8080;
6+
7+
const targets = await CDP.List({ host, port });
8+
9+
const target = targets.find(t => t.title.includes('SteamDB'));
10+
11+
if (!target) {
12+
console.log('❌ No target found');
13+
14+
return;
15+
}
16+
17+
const client = await CDP({ target, host, port });
18+
const { Runtime } = client;
19+
20+
await Runtime.enable(); // ✅ Actually wait for Runtime to be enabled
21+
22+
Runtime.exceptionThrown(({ exceptionDetails }) => {
23+
console.log(exceptionDetails);
24+
console.log('💥 Exception caught:');
25+
console.log('Message:', exceptionDetails.text);
26+
27+
if (exceptionDetails.stackTrace?.callFrames) {
28+
console.log('📚 Stack trace:');
29+
for (const frame of exceptionDetails.stackTrace.callFrames) {
30+
console.log(` at ${frame.functionName || '<anonymous>'} (${frame.url}:${frame.lineNumber + 1}:${frame.columnNumber + 1})`);
31+
}
32+
}
33+
});
34+
35+
console.log('🧠 Ready to catch errors...');
36+
37+
// Force an error in the page to test (run in page context)
38+
await Runtime.evaluate({
39+
expression: 'setTimeout(() => { throw new Error("Injected Failstorm™️") }, 1000);',
40+
});
41+
42+
// Keep the process alive like your last two brain cells
43+
process.stdin.resume();
44+
}
45+
46+
main().catch((e) => { console.error('💀 Error in main():', e); });

frontend/browser/corsFetch.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { SteamRequestResponseContent } from '../extension/websocket/MessageTypes
44
import { mainWindow } from '../shared';
55

66
/* eslint-disable @typescript-eslint/no-base-to-string */
7-
const corsCacheKey = 'extendium-cors-cache';
7+
const corsCacheKey = 'extendium_cors_cache';
88
const proxyUrl = 'http://127.0.0.1:8792/proxy/';
99

1010
const pendingRequests = new Map<string, PendingRequest>();

frontend/components/stores/extensionsBarStore.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ interface ExtensionsBarStore {
66
extensionsOrder: string[];
77
}
88

9-
const storageKey = 'extendium_extensionsBarStore';
9+
export const extensionBarStorageKey = 'extendium_extensionsBarStore';
1010

1111
export const useExtensionsBarStore = create<ExtensionsBarStore>()(persist(
1212
set =>
@@ -19,6 +19,6 @@ export const useExtensionsBarStore = create<ExtensionsBarStore>()(persist(
1919
},
2020
}),
2121
{
22-
name: storageKey,
22+
name: extensionBarStorageKey,
2323
},
2424
));

frontend/extensions-manager/ExtensionDetailInfo.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export function ExtensionDetailInfo({ extension }: { readonly extension: Extensi
4949
return (
5050
<div className="extension-detail-info">
5151
<div className="page-header">
52-
<button onClick={() => { setManagerPopup({ route: null }); }} type="button">
52+
<button onClick={() => { setManagerPopup({ route: null }); }} type="button" className="md-button">
5353
<MdArrowBack />
5454
</button>
5555
<img src={extension.action.getDefaultIconUrl(48)} />

frontend/extensions-manager/ExtensionManagerComponent.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export function ExtensionManagerComponent({ extension }: { readonly extension: E
3838
</div>
3939
</div>
4040
<div className="extension-buttons">
41-
<DialogButton onClick={() => { setManagerPopup({ route: extension.getName() }); }}>Details</DialogButton>
41+
<DialogButton onClick={() => { setManagerPopup({ route: `info/${extension.getName()}` }); }}>Details</DialogButton>
4242
<DialogButton onClick={() => { showRemoveModal(extension); }}>Remove</DialogButton>
4343
{/* @ts-expect-error style does not exist */}
4444
<Toggle onChange={handleToggleChange} style={{ display: 'none' }} value />

frontend/extensions-manager/ExtensionManagerPopup.tsx

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { default as React } from 'react';
77
import { mainWindow } from 'shared';
88
import { ExtensionDetailInfo } from './ExtensionDetailInfo';
99
import { ExtensionManagerRoot } from './ExtensionManagerRoot';
10+
import { StorageManager } from './Storage/StorageManager';
1011

1112
export function ExtensionManagerPopup(): React.ReactNode {
1213
const { managerPopup, setManagerPopup } = usePopupsStore();
@@ -16,6 +17,16 @@ export function ExtensionManagerPopup(): React.ReactNode {
1617
return null;
1718
}
1819

20+
let content: React.ReactNode = null;
21+
22+
if (managerPopup.route === null) {
23+
content = <ExtensionManagerRoot />;
24+
} else if (managerPopup.route.startsWith('info/')) {
25+
content = <ExtensionDetailInfo extension={extensions.get(managerPopup.route.split('/').pop() ?? '')} />;
26+
} else if (managerPopup.route.startsWith('storage')) {
27+
content = <StorageManager />;
28+
}
29+
1930
return (
2031
<SteamDialog
2132
strTitle="Extensions"
@@ -29,13 +40,7 @@ export function ExtensionManagerPopup(): React.ReactNode {
2940
>
3041
<Styles />
3142
<div className="extension-manager-popup">
32-
{managerPopup.route !== null
33-
? (
34-
<ExtensionDetailInfo extension={extensions.get(managerPopup.route)} />
35-
)
36-
: (
37-
<ExtensionManagerRoot />
38-
)}
43+
{content}
3944
</div>
4045
</SteamDialog>
4146
);

frontend/extensions-manager/ExtensionManagerRoot.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import { callable, DialogButton } from '@steambrew/client';
22
import { DialogControlSectionClass, settingsClasses } from 'classes';
3+
import { usePopupsStore } from 'components/stores/popupsStore';
34
import React from 'react';
4-
import { FaFolderOpen, FaStore } from 'react-icons/fa';
5+
import { FaDatabase, FaFolderOpen, FaStore } from 'react-icons/fa';
56
import { ExtensionManagerComponent } from './ExtensionManagerComponent';
67
import { showInstallExtensionModal } from './InstallExtensionModal';
78

89
const GetExtensionsDir = callable<[], string>('GetExtensionsDir');
910

1011
export function ExtensionManagerRoot(): React.ReactNode {
12+
const { setManagerPopup } = usePopupsStore();
13+
1114
async function openExtensionsFolder(): Promise<void> {
1215
const extensionsDir = await GetExtensionsDir();
1316
SteamClient.System.OpenLocalDirectoryInSystemExplorer(extensionsDir);
@@ -20,6 +23,13 @@ export function ExtensionManagerRoot(): React.ReactNode {
2023
<FaSave />
2124
Save
2225
</DialogButton> */}
26+
<DialogButton
27+
onClick={() => { setManagerPopup({ route: 'storage' }); }}
28+
className={`span-icon ${settingsClasses.SettingsDialogButton}`}
29+
>
30+
<FaDatabase />
31+
Manage storage
32+
</DialogButton>
2333
<DialogButton
2434
onClick={showInstallExtensionModal}
2535
className={`span-icon ${settingsClasses.SettingsDialogButton}`}

frontend/extensions-manager/RemoveModal.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Extension } from '@extension/Extension';
22
import { callable } from '@steambrew/client';
3+
import { useExtensionsBarStore } from 'components/stores/extensionsBarStore';
34
import React from 'react';
45
import { showConfirmationModal } from '../components/ConfirmationModal';
56
import { showRestartModal } from './RestartModal';
@@ -14,9 +15,18 @@ export function showRemoveModal(extension: Extension): void {
1415
),
1516
okButtonText: 'Remove',
1617
onOK: async () => {
17-
await RemoveExtension({ name: extension.folderName });
18+
await removeExtension(extension);
1819
showRestartModal();
1920
},
2021
bNeverPopOut: true,
2122
});
2223
}
24+
25+
async function removeExtension(extension: Extension): Promise<void> {
26+
await RemoveExtension({ name: extension.folderName });
27+
const order = useExtensionsBarStore.getState().extensionsOrder;
28+
if (order.includes(extension.folderName)) {
29+
order.splice(order.indexOf(extension.folderName), 1);
30+
useExtensionsBarStore.setState({ extensionsOrder: order });
31+
}
32+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { showConfirmationModal } from 'components/ConfirmationModal';
2+
import { extensionBarStorageKey, useExtensionsBarStore } from 'components/stores/extensionsBarStore';
3+
import { usePopupsStore } from 'components/stores/popupsStore';
4+
import React from 'react';
5+
import { MdArrowBack, MdDelete, MdRefresh } from 'react-icons/md';
6+
7+
export function StorageManager(): React.ReactNode {
8+
const [storages, setStorages] = React.useState<[string, unknown][]>([]);
9+
const { setManagerPopup } = usePopupsStore();
10+
11+
React.useEffect(() => {
12+
refreshStorages();
13+
}, []);
14+
15+
function refreshStorages(): void {
16+
const storage: [string, unknown][] = Object.entries(localStorage).filter(([key]) =>
17+
key.endsWith('::local')
18+
|| key.endsWith('::session')
19+
|| key.endsWith('::sync')
20+
|| key.endsWith('::managed')).sort(([keyA], [keyB]) => keyA.localeCompare(keyB));
21+
storage.push(...Object.entries(localStorage).filter(([key]) => key.includes('extendium')));
22+
setStorages(storage);
23+
}
24+
25+
function renderObject(obj: object, keyStr: string, depth = 0): React.ReactNode {
26+
return (
27+
<div className="storage-manager-item" style={{ paddingLeft: `${depth * 10}px` }}>
28+
<details key={keyStr}>
29+
<summary>{keyStr}</summary>
30+
{Object.entries(obj).map(([key, value]) => (
31+
typeof value === 'object' && value !== null
32+
? renderObject(value as object, key, depth + 1)
33+
: <div className="storage-manager-item" style={{ paddingLeft: `${(depth + 1) * 10}px` }} key={key}>{key}: {String(value)}</div>
34+
))}
35+
</details>
36+
</div>
37+
);
38+
}
39+
40+
function removeStorage(key: string): void {
41+
showConfirmationModal({
42+
title: 'Clear storage',
43+
description: (
44+
<p>Are you sure you want to clear out the <br /> &quot;{formatStorageKey(key)}&quot;?</p>
45+
),
46+
okButtonText: 'Clear',
47+
onOK: () => {
48+
localStorage.removeItem(key);
49+
if (key === extensionBarStorageKey) {
50+
useExtensionsBarStore.setState({ extensionsOrder: [] });
51+
}
52+
refreshStorages();
53+
},
54+
bNeverPopOut: true,
55+
});
56+
}
57+
58+
function formatStorageKey(key: string): string {
59+
const [extensionId, area] = key.split('::');
60+
61+
if (area === undefined) {
62+
return extensionId ?? 'Unknown';
63+
}
64+
65+
return `${extensionId} (${area} storage)`;
66+
}
67+
68+
return (
69+
<div className="storage-manager">
70+
<div className="header">
71+
<button onClick={() => { setManagerPopup({ route: null }); }} type="button" className="md-button">
72+
<MdArrowBack />
73+
</button>
74+
<h1>Storage Manager</h1>
75+
<button type="button" onClick={() => { refreshStorages(); }} className="md-button"><MdRefresh /></button>
76+
</div>
77+
<p>
78+
Here you can manage all data that Extendium has stored for each extension.
79+
<br />
80+
This is useful if you want to clear out all data for a specific extension if for example it is not working properly or if you want to reset settings.
81+
<br />
82+
All this data is stored in the Steam browser&apos;s local storage and can be accessed through the browser&apos;s developer tools on any steamloopback address like the extension backgrounds.
83+
</p>
84+
85+
{storages.map(([key, value]) => (
86+
<div key={key} className="storage-manager-row">
87+
{renderObject(JSON.parse(value as string) as object, formatStorageKey(key))}
88+
<button type="button" onClick={() => { removeStorage(key); }}><MdDelete /></button>
89+
</div>
90+
))}
91+
</div>
92+
);
93+
}

public/extendium.scss

Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,19 @@ $grid-gutter: 12px;
198198
}
199199
}
200200

201+
.md-button {
202+
@include icon-button;
203+
204+
transition: background 0.2s ease;
205+
border-radius: 100px;
206+
width: 32px;
207+
height: 32px;
208+
209+
&:hover {
210+
background: $button-hover-background;
211+
}
212+
}
213+
201214
.extension-detail-info {
202215
width: 680px;
203216
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.5);
@@ -211,19 +224,6 @@ $grid-gutter: 12px;
211224
padding: 8px 12px 0px;
212225
color: $primary-text-color;
213226

214-
& > button {
215-
@include icon-button;
216-
217-
transition: background 0.2s ease;
218-
border-radius: 100px;
219-
width: 32px;
220-
height: 32px;
221-
222-
&:hover {
223-
background: $button-hover-background;
224-
}
225-
}
226-
227227
& > img {
228228
margin-inline: 16px 12px;
229229
height: 24px;
@@ -288,4 +288,42 @@ $grid-gutter: 12px;
288288
border-top: 1px solid rgba(255, 255, 255, 0.1);
289289
}
290290

291-
//#endregion
291+
//#endregion
292+
293+
.storage-manager {
294+
color: $primary-text-color;
295+
.storage-manager-row {
296+
@include flex-between;
297+
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
298+
299+
button {
300+
@include icon-button;
301+
cursor: pointer;
302+
align-self: flex-start;
303+
height: 34px;
304+
width: 34px;
305+
padding: 5px;
306+
}
307+
}
308+
309+
.storage-manager-item {
310+
width: 100%;
311+
padding: 5px 0;
312+
313+
&:not(:has(> details[open])) {
314+
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
315+
}
316+
}
317+
318+
.header {
319+
display: flex;
320+
align-items: center;
321+
gap: 1rem;
322+
color: white;
323+
324+
h1 {
325+
margin: 0;
326+
}
327+
}
328+
329+
}

0 commit comments

Comments
 (0)