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+ }
0 commit comments