Skip to content

Commit 91f317f

Browse files
committed
feat: Replace bespoke fileserver with fastify
feat: Add config to main state feat: WIP components list in manager ext
1 parent c7bc42b commit 91f317f

14 files changed

Lines changed: 1194 additions & 618 deletions

File tree

extensions/core-manager/package-lock.json

Lines changed: 6 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

extensions/core-manager/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
"@mdx-js/mdx": "3.1.0",
2626
"@mdx-js/react": "3.1.0",
2727
"@reduxjs/toolkit": "2.7.0",
28-
"fast-xml-parser": "^5.2.5",
2928
"mdast-util-mdx-jsx": "^3.2.0",
3029
"react": "19.1.0",
3130
"react-dom": "19.1.0",
@@ -42,6 +41,7 @@
4241
"@types/node": "18.x",
4342
"babel-plugin-react-compiler": "19.1.0-rc.1",
4443
"esbuild": "^0.25.12",
44+
"fast-xml-parser": "^5.3.3",
4545
"glob": "^11.0.2",
4646
"gulp": "5.0.1",
4747
"gulp-zip": "^5.0.2",
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import axios from 'axios';
2+
import { XMLParser } from 'fast-xml-parser';
3+
import { useAppSelector } from 'flashpoint-launcher-renderer-ext/hooks';
4+
import fs from 'fs';
5+
import path from 'path';
6+
import { useEffect, useState } from 'react';
7+
import { selectComponentRootUrls } from '../select';
8+
9+
export function ComponentSubsection() {
10+
const [remoteInfo, setRemoteInfo] = useState<ManagerComponentRemoteInfo[]>([]);
11+
const [installedInfo, setInstalledInfo] = useState<Record<string, ManagerInstalledComponentInfo>>({});
12+
const [ready, setReady] = useState(false);
13+
const componentsPath = useAppSelector(state => path.join(state.main.config.flashpointPath, 'Components'));
14+
const remoteComponentUrlsRaw = useAppSelector(selectComponentRootUrls);
15+
16+
useEffect(() => {
17+
readInstalledComponents(componentsPath)
18+
.then((data) => {
19+
setInstalledInfo(data);
20+
setReady(true);
21+
})
22+
}, [componentsPath]);
23+
24+
useEffect(() => {
25+
const repoUrls = remoteComponentUrlsRaw
26+
.split('\n')
27+
.map(url => url.trim())
28+
.filter(url => url.length > 0);
29+
Promise.all(repoUrls.map(getRemoteFromIndex))
30+
.then((responses) => {
31+
const data = responses.reduce((prev, cur) => prev.concat(cur), []);
32+
setRemoteInfo(data);
33+
})
34+
}, [remoteComponentUrlsRaw]);
35+
36+
const componentList: Record<string, ManagerComponent> = {};
37+
if (ready) {
38+
for (const key in installedInfo) {
39+
componentList[key] = {
40+
installed: installedInfo[key],
41+
canUpdate: false,
42+
updateDiff: 0,
43+
};
44+
}
45+
for (const remote of remoteInfo) {
46+
const comp = componentList[remote.id];
47+
if (comp) {
48+
comp.remote = remote;
49+
const installedSize = comp.installed ? comp.installed.size : 0;
50+
const installedHash = comp.installed ? comp.installed.hash : '';
51+
if (installedHash.toLowerCase() !== remote.hash.toLowerCase()) {
52+
comp.canUpdate = true;
53+
comp.updateDiff = installedSize - remote.installSize;
54+
}
55+
} else {
56+
componentList[remote.id] = {
57+
remote,
58+
canUpdate: true,
59+
updateDiff: remote.installSize
60+
}
61+
}
62+
}
63+
}
64+
65+
}
66+
67+
async function readInstalledComponents(componentsPath: string): Promise<Record<string, ManagerInstalledComponentInfo>> {
68+
await fs.promises.mkdir(componentsPath, { recursive: true });
69+
const files = await fs.promises.readdir(componentsPath);
70+
const components: Record<string, ManagerInstalledComponentInfo> = {};
71+
for (const file of files) {
72+
try {
73+
const filePath = path.join(componentsPath, file);
74+
const content = await fs.promises.readFile(filePath, { encoding: 'utf-8' });
75+
const lines = content.split('\n');
76+
const [hash, size] = lines[0].split(' ');
77+
if (hash.length !== 8) {
78+
throw 'Hash length invalid';
79+
}
80+
components[file] = {
81+
size: parseInt(size),
82+
hash,
83+
fileCount: lines.length - 1
84+
};
85+
} catch (err) {
86+
log.error('Manager', 'Failed to read component: ' + file);
87+
}
88+
}
89+
return components;
90+
}
91+
92+
async function getRemoteFromIndex(indexUrl: string): Promise<ManagerComponentRemoteInfo[]> {
93+
const components: ManagerComponentRemoteInfo[] = [];
94+
const parser = new XMLParser({
95+
ignoreAttributes: false
96+
});
97+
98+
const res = await axios.get(indexUrl);
99+
if (res.status < 300) {
100+
const data = parser.parse(res.data);
101+
102+
function processCategory(category: any, parentId: string = '') {
103+
const categoryId = parentId ? `${parentId}-${category.id}` : category.id;
104+
105+
// Process nested categories
106+
if (category.category) {
107+
const categories = Array.isArray(category.category) ? category.category : [category.category];
108+
categories.forEach((cat: any) => processCategory(cat, categoryId));
109+
}
110+
111+
// Process components in this category
112+
if (category.component) {
113+
const comps = Array.isArray(category.component) ? category.component : [category.component];
114+
comps.forEach((comp: any) => {
115+
const componentId = `${categoryId}-${comp.id}`;
116+
const baseUrl = data.list.url || indexUrl.substring(0, indexUrl.lastIndexOf('/') + 1);
117+
118+
components.push({
119+
id: componentId,
120+
title: comp.title || '',
121+
description: comp.description || '',
122+
dateModified: comp['date-modified'] || '',
123+
downloadSize: parseInt(comp['download-size']) || 0,
124+
installSize: parseInt(comp['install-size']) || 0,
125+
path: comp.path || '',
126+
hash: comp.hash || '',
127+
downloadUrl: `${baseUrl}${componentId}.7z`
128+
});
129+
});
130+
}
131+
}
132+
133+
if (data.list && data.list.category) {
134+
const rootCategories = Array.isArray(data.list.category) ? data.list.category : [data.list.category];
135+
rootCategories.forEach((cat: any) => processCategory(cat));
136+
}
137+
} else {
138+
throw 'Bad status code: ' + res.status;
139+
}
140+
141+
return components;
142+
}
143+
144+
type ManagerComponent = {
145+
installed?: ManagerInstalledComponentInfo;
146+
remote?: ManagerComponentRemoteInfo;
147+
canUpdate: boolean;
148+
updateDiff: number;
149+
}
150+
151+
type ManagerInstalledComponentInfo = {
152+
size: number;
153+
hash: string;
154+
fileCount: number;
155+
}
156+
157+
type ManagerComponentRemoteInfo = {
158+
id: string;
159+
title: string;
160+
description: string;
161+
dateModified: string;
162+
downloadSize: number;
163+
installSize: number;
164+
path: string;
165+
hash: string;
166+
downloadUrl: string;
167+
}

extensions/core-manager/src/select.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@ import { RootState } from 'flashpoint-launcher-renderer';
22
import { setExtConfigValue } from 'flashpoint-launcher-renderer-ext/actions/main';
33

44
const DEFAULT_REPO_URLS = 'https://raw.githubusercontent.com/FlashpointProject/FlashpointExtensionIndex/refs/heads/main/extindex.json';
5+
const DEFAULT_COMPONENT_ROOTS_URLS = 'https://nexus-dev.flashpointarchive.org/repository/components-stable/components.xml';
56

67
const CONFIG_KEY_REPOS = 'core-manager.repositories';
8+
const CONFIG_KEY_COMPONENT_ROOTS = 'core-manager.component-roots';
79

810
export const selectRepoUrls = (state: RootState) => (state.main.extConfig[CONFIG_KEY_REPOS] as string) || DEFAULT_REPO_URLS;
9-
export const setRepoUrlsAction = (repos: string) => setExtConfigValue({ key: CONFIG_KEY_REPOS, value: repos });
11+
export const setRepoUrlsAction = (repos: string) => setExtConfigValue({ key: CONFIG_KEY_REPOS, value: repos });
12+
export const selectComponentRootUrls = (state: RootState) => (state.main.extConfig[CONFIG_KEY_COMPONENT_ROOTS] as string) || DEFAULT_COMPONENT_ROOTS_URLS;

0 commit comments

Comments
 (0)