@@ -79,26 +79,33 @@ const StorageHub = ({ payloads, payloadMeta, onInstall, onDelete, onUpload, onIm
7979 } ) . sort ( ( a , b ) => {
8080 if ( a . isUpdate && ! b . isUpdate ) return - 1
8181 if ( ! a . isUpdate && b . isUpdate ) return 1
82- return 0
82+
83+ const nameA = ( a . name || a . filename || '' ) . toLowerCase ( )
84+ const nameB = ( b . name || b . filename || '' ) . toLowerCase ( )
85+ return nameA . localeCompare ( nameB )
8386 } )
8487
85- /* ---- Source-grouped data (multi-source mode) ---- */
88+ /* ---- Source-grouped data ---- */
8689 const enrichedSources = useMemo ( ( ) => {
87- if ( ! repoData ?. sources ) return [ ]
88- return repoData . sources . map ( src => ( {
89- ...src ,
90- payloads : enrichPayloads ( src . payloads || [ ] )
91- } ) )
92- } , [ repoData , localFilenames ] )
93-
94- /* ---- Flat list (legacy single-source mode) ---- */
95- const remotePayloads = useMemo ( ( ) => {
96- if ( ! repoData ?. payloads ) return [ ]
97- return enrichPayloads ( repoData . payloads )
98- } , [ repoData , localFilenames ] )
90+ if ( multiSources && repoData ?. sources ) {
91+ return repoData . sources . map ( src => ( {
92+ ...src ,
93+ id : src . id || src . url ,
94+ payloads : enrichPayloads ( src . payloads || [ ] )
95+ } ) )
96+ } else if ( ! multiSources && repoData ?. payloads ) {
97+ return [ {
98+ id : 'legacy-repo' ,
99+ name : 'Default Repository' ,
100+ url : repoData . repo_url || '' ,
101+ last_update : repoData . last_update || 0 ,
102+ payloads : enrichPayloads ( repoData . payloads )
103+ } ]
104+ }
105+ return [ ]
106+ } , [ repoData , multiSources , localFilenames ] )
99107
100108 const legacyRepoUrl = repoData ?. repo_url || ''
101- const legacyLastUpdate = Number ( repoData ?. last_update || 0 )
102109
103110 /* ---- Source badge helper: look up source name from metadata ---- */
104111 const getSourceBadge = ( fileName ) => {
@@ -146,9 +153,7 @@ const StorageHub = ({ payloads, payloadMeta, onInstall, onDelete, onUpload, onIm
146153 const fileName = path . split ( '/' ) . pop ( )
147154 const sourceBadge = getSourceBadge ( fileName )
148155 // Find update in all sources (multi or legacy)
149- const allRemote = multiSources
150- ? enrichedSources . flatMap ( s => s . payloads )
151- : remotePayloads
156+ const allRemote = enrichedSources . flatMap ( s => s . payloads )
152157 const remoteMatch = allRemote . find ( rp => rp . filename === fileName || rp . installedFilename === fileName )
153158 const remoteVersion = remoteMatch ?. filename ? parsePayloadName ( remoteMatch . filename ) . version : null
154159 return (
@@ -220,19 +225,31 @@ const StorageHub = ({ payloads, payloadMeta, onInstall, onDelete, onUpload, onIm
220225 </ div >
221226 < button onClick = { ( ) => fetchRemote ( true ) } className = "px-8 py-3 bg-white/5 border border-white/10 hover:bg-white/10 text-white rounded-xl font-bold uppercase text-xs transition-all" > Retry Connection</ button >
222227 </ div >
223- ) : multiSources && enrichedSources . length > 0 ? (
224- /* ===== MULTI-SOURCE: accordion catalogs ===== */
228+ ) : enrichedSources . length > 0 ? (
229+ /* ===== REPOSITORY CATALOGS ===== */
225230 < div className = "space-y-4" >
226231 { enrichedSources . map ( src => {
227232 const availablePayloads = src . payloads . filter ( p => ! p . isInstalled || p . isUpdate )
228- const isExpanded = expandedSource === src . id
233+ // Auto-expand if there's only 1 source, otherwise respect state
234+ const isExpanded = ( enrichedSources . length === 1 ) || expandedSource === src . id
235+
229236 return (
230- < div key = { src . id } className = "bg-ps-card border border-ps-border rounded-ps-3xl overflow-hidden" >
231- { /* Catalog header */ }
232- < button
233- onClick = { ( ) => setExpandedSource ( isExpanded ? null : src . id ) }
234- className = "w-full flex items-center justify-between p-6 md:p-8 hover:bg-white/5 transition-colors"
235- >
237+ < div key = { src . id } className = { cn (
238+ multiSources ? "bg-ps-card border border-ps-border rounded-ps-3xl overflow-hidden" : "flex flex-col space-y-4"
239+ ) } >
240+ { /* Last Sync for single-source mode */ }
241+ { ! multiSources && src . last_update > 0 && (
242+ < p className = "px-2 text-xs uppercase tracking-widest text-zinc-500" >
243+ Last Sync: { new Date ( src . last_update * 1000 ) . toLocaleString ( ) }
244+ </ p >
245+ ) }
246+
247+ { /* Catalog header (only for multi-source) */ }
248+ { multiSources && (
249+ < button
250+ onClick = { ( ) => setExpandedSource ( isExpanded ? null : src . id ) }
251+ className = "w-full flex items-center justify-between p-6 md:p-8 hover:bg-white/5 transition-colors"
252+ >
236253 < div className = "flex items-center space-x-4" >
237254 < div className = "p-2.5 bg-ps-blue/10 rounded-xl" >
238255 < Globe className = "w-5 h-5 text-ps-blue" />
@@ -245,8 +262,8 @@ const StorageHub = ({ payloads, payloadMeta, onInstall, onDelete, onUpload, onIm
245262 < span > Fetch failed</ span >
246263 </ p >
247264 ) }
248- { ! src . error && src . last_update > 0 && (
249- < p className = "text-xs text-zinc-600 mt-0.5 " >
265+ { src . last_update > 0 && (
266+ < p className = "text-xs text-zinc-500 uppercase tracking-widest mt-1 " >
250267 Updated { new Date ( src . last_update * 1000 ) . toLocaleString ( ) }
251268 </ p >
252269 ) }
@@ -256,36 +273,46 @@ const StorageHub = ({ payloads, payloadMeta, onInstall, onDelete, onUpload, onIm
256273 < span className = "px-3 py-1 rounded-full bg-white/5 text-zinc-500 text-xs font-bold" >
257274 { availablePayloads . length } available
258275 </ span >
259- < ChevronDown className = { cn ( "w-5 h-5 text-zinc-500 transition-transform" , isExpanded && "rotate-180" ) } />
276+ { enrichedSources . length > 1 && (
277+ < ChevronDown className = { cn ( "w-5 h-5 text-zinc-500 transition-transform" , isExpanded && "rotate-180" ) } />
278+ ) }
260279 </ div >
261280 </ button >
281+ ) }
262282
263283 { /* Catalog payload list */ }
264284 { isExpanded && (
265- < div className = "border-t border-white/5 divide-y divide-white/5" >
266- { availablePayloads . length === 0 ? (
285+ < div className = { cn (
286+ multiSources ? "border-t border-white/5 divide-y divide-white/5" : "grid grid-cols-1 gap-4"
287+ ) } >
288+ { src . payloads . length === 0 ? (
267289 < div className = "py-12 flex flex-col items-center justify-center space-y-3 text-zinc-600" >
268- < p className = "text-sm font-bold uppercase tracking-widest italic" >
269- { src . payloads . length === 0 ? "Source is empty" : "All payloads installed" }
270- </ p >
290+ < p className = "text-sm font-bold uppercase tracking-widest italic" > Source is empty</ p >
291+ </ div >
292+ ) : availablePayloads . length === 0 ? (
293+ < div className = "py-12 flex flex-col items-center justify-center space-y-3 text-zinc-600" >
294+ < p className = "text-sm font-bold uppercase tracking-widest italic" > All payloads installed</ p >
271295 </ div >
272296 ) : (
273297 availablePayloads . map ( p => (
274298 < div
275299 key = { p . filename }
276300 className = { cn (
277- "flex flex-col md:flex-row justify-between gap-4 md:gap-8 p-6 md:p-8 hover:bg-white/[0.03] transition-colors" ,
301+ "flex flex-col md:flex-row justify-between gap-4 md:gap-8 p-6 md:p-8 transition-all" ,
302+ multiSources
303+ ? "hover:bg-white/[0.03]"
304+ : "glass-card rounded-ps-3xl border border-white/10 hover:border-ps-blue/20 bg-white/[0.01]" ,
278305 isPS5 ? "flex-row items-center" : "items-start md:items-center"
279306 ) }
280307 >
281308 < div className = "space-y-2 min-w-0" >
282- < PayloadName path = { p . filename } className = "text-xl md:text-2xl text-white" stacked />
309+ < PayloadName path = { p . filename } className = "text-xl md:text-2xl text-white" stacked lastUpdate = { p . last_update } />
283310 { p . description && (
284311 < p className = "text-sm md:text-base text-zinc-400 font-medium leading-relaxed" > { p . description } </ p >
285312 ) }
286313 </ div >
287314 < button
288- onClick = { ( ) => onInstall ( p , p . source_id , src . url ) }
315+ onClick = { ( ) => onInstall ( p , src . id === 'legacy-repo' ? null : src . id , src . url ) }
289316 className = { cn (
290317 "flex items-center justify-center space-x-3 px-6 md:px-8 py-3 md:py-5 rounded-2xl font-bold text-lg transition-all shrink-0 transform active:scale-95" ,
291318 isPS5 ? "w-auto px-12" : "w-full md:w-auto" ,
@@ -307,50 +334,9 @@ const StorageHub = ({ payloads, payloadMeta, onInstall, onDelete, onUpload, onIm
307334 } ) }
308335 </ div >
309336 ) : (
310- /* ===== SINGLE-SOURCE: flat list (legacy / multi-source disabled) ===== */
311- < div className = "grid grid-cols-1 gap-4" >
312- { legacyLastUpdate > 0 && (
313- < p className = "px-2 text-xs uppercase tracking-widest text-zinc-500" >
314- Last Sync: { new Date ( legacyLastUpdate * 1000 ) . toLocaleString ( ) }
315- </ p >
316- ) }
317- { ( ( ) => {
318- const cloudItems = remotePayloads . filter ( p => ! p . isInstalled || p . isUpdate )
319- return remotePayloads . length === 0 ? (
320- < div className = "py-20 border-2 border-dashed border-white/5 rounded-ps-3xl flex flex-col items-center justify-center space-y-4 bg-white/[0.01]" >
321- < p className = "text-zinc-500 font-bold uppercase tracking-widest text-sm italic" > Repository is empty</ p >
322- </ div >
323- ) : cloudItems . length === 0 ? (
324- < div className = "py-20 border-2 border-dashed border-white/5 rounded-ps-3xl flex flex-col items-center justify-center space-y-4 bg-white/[0.01]" >
325- < p className = "text-zinc-500 font-bold uppercase tracking-widest text-sm italic" > All payloads installed</ p >
326- </ div >
327- ) : (
328- cloudItems . map ( p => (
329- < div key = { p . filename } className = { cn (
330- "glass-card p-6 md:p-8 rounded-ps-3xl flex flex-col md:flex-row justify-between gap-4 md:gap-8 border-white/10 hover:border-ps-blue/20 transition-all bg-white/[0.01]" ,
331- isPS5 ? "flex-row items-center" : "items-start md:items-center"
332- ) } >
333- < div className = "space-y-2 md:space-y-3 min-w-0" >
334- < div className = "flex items-center space-x-4" >
335- < PayloadName path = { p . filename } className = "text-xl md:text-2xl text-white" stacked />
336- </ div >
337- < p className = "text-sm md:text-lg text-zinc-400 font-medium max-w-3xl leading-relaxed" > { p . description } </ p >
338- </ div >
339- < button
340- onClick = { ( ) => onInstall ( p , null , legacyRepoUrl ) }
341- className = { cn (
342- "flex items-center justify-center space-x-3 md:space-x-4 px-6 md:px-8 py-3 md:py-5 rounded-2xl font-bold text-lg md:text-xl transition-all shrink-0 transform active:scale-95" ,
343- isPS5 ? "w-auto px-12" : "w-full md:w-auto" ,
344- p . isUpdate ? "bg-emerald-600 hover:bg-emerald-500 text-white" : "bg-ps-blue hover:bg-ps-blue/80 text-white"
345- ) }
346- >
347- < CloudDownload className = "w-5 h-5 md:w-7 md:h-7" />
348- < span > { p . isUpdate ? "Update" : "Install" } </ span >
349- </ button >
350- </ div >
351- ) )
352- )
353- } ) ( ) }
337+ < div className = "py-20 border-2 border-dashed border-white/5 rounded-ps-3xl flex flex-col items-center justify-center space-y-4 bg-white/[0.01]" >
338+ < CloudDownload className = "w-16 h-16 text-white/5" />
339+ < p className = "text-zinc-500 font-bold uppercase tracking-widest text-sm italic" > Repository is empty</ p >
354340 </ div >
355341 ) }
356342 </ section >
0 commit comments