Skip to content

Commit 293f44b

Browse files
committed
feat(StorageHub): unify repository UI, add alphabetical sorting and payload update dates
- Refactored `StorageHub.jsx` to share a unified `enrichedSources` data flow between single-source and multi-source modes, eliminating duplicated rendering logic - Payloads are now sorted alphabetically by name across all repository views (with available updates automatically pinned to the top) - Updated `PayloadName` component to accept a `lastUpdate` prop, natively rendering the payload version and update date side-by-side - Conditionally hide accordion headers and restore discrete grid-card styling for legacy single-source mode, preserving the original look while utilizing the normalized codebase - Fixed a backend bug in `src/sources.c` where the `last_update` field was incorrectly omitted from the serialized JSON array during multi-source repository fetches
1 parent b8abb0a commit 293f44b

3 files changed

Lines changed: 93 additions & 93 deletions

File tree

frontend/src/components/ui/PayloadName.jsx

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React from 'react'
22
import { Zap, Usb } from 'lucide-react'
33
import { cn, parsePayloadName } from '../../utils/helpers'
44

5-
const PayloadName = ({ path, className, versionClassName, stacked = false, hideIcon = false }) => {
5+
const PayloadName = ({ path, className, versionClassName, stacked = false, hideIcon = false, lastUpdate = null }) => {
66
const { displayName, version, isDelay } = parsePayloadName(path);
77
const isUsb = path?.startsWith('/mnt/usb');
88

@@ -13,14 +13,26 @@ const PayloadName = ({ path, className, versionClassName, stacked = false, hideI
1313
{isUsb && !hideIcon && <Usb className="w-5 h-5 text-ps-blue shrink-0 mr-1" />}
1414
<span className="font-bold truncate shrink leading-tight">{displayName}</span>
1515
</div>
16-
{version && (
17-
<span className={cn(
18-
stacked
19-
? "text-[11px] font-bold tracking-wider text-ps-blue mt-1 opacity-90 self-start"
20-
: "text-[10px] px-2 py-0.5 bg-ps-blue/10 text-ps-blue font-bold rounded-md border border-ps-blue/20 shrink-0",
21-
versionClassName)}>
22-
{version}
23-
</span>
16+
{(version || lastUpdate) && (
17+
<div className={cn("flex items-center", stacked ? "mt-1" : "")}>
18+
{version && (
19+
<span className={cn(
20+
stacked
21+
? "text-[11px] font-bold tracking-wider text-ps-blue opacity-90"
22+
: "text-[10px] px-2 py-0.5 bg-ps-blue/10 text-ps-blue font-bold rounded-md border border-ps-blue/20 shrink-0",
23+
versionClassName)}>
24+
{version}
25+
</span>
26+
)}
27+
{version && lastUpdate && (
28+
<span className="text-[11px] text-zinc-600 mx-2"></span>
29+
)}
30+
{lastUpdate && (
31+
<span className="text-[11px] font-bold tracking-wider text-zinc-500 uppercase">
32+
{lastUpdate}
33+
</span>
34+
)}
35+
</div>
2436
)}
2537
</div>
2638
);

frontend/src/components/views/StorageHub.jsx

Lines changed: 68 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -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>

src/sources.c

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -444,19 +444,21 @@ size_t sources_multi_repository_list_json(char *buf, size_t size, int force_refr
444444
for (size_t pi = 0; pi < count; pi++) {
445445
char name_pe[256], filename_e[512], desc_e[2048];
446446
char ver_e[128], url_e[2048], src_id_e[128], src_name_e[512];
447+
char last_upd_e[128];
447448

448449
pldmgr_json_escape(items[pi].name, name_pe, sizeof(name_pe));
449450
pldmgr_json_escape(items[pi].filename, filename_e, sizeof(filename_e));
450451
pldmgr_json_escape(items[pi].description, desc_e, sizeof(desc_e));
451452
pldmgr_json_escape(items[pi].version, ver_e, sizeof(ver_e));
452453
pldmgr_json_escape(items[pi].url, url_e, sizeof(url_e));
454+
pldmgr_json_escape(items[pi].last_update, last_upd_e, sizeof(last_upd_e));
453455
pldmgr_json_escape(sources[si].id, src_id_e, sizeof(src_id_e));
454456
pldmgr_json_escape(sources[si].name, src_name_e, sizeof(src_name_e));
455457

456458
if (json_append(&jb, " {\"name\":\"%s\",\"filename\":\"%s\",\"description\":\"%s\","
457-
"\"version\":\"%s\",\"url\":\"%s\","
459+
"\"version\":\"%s\",\"last_update\":\"%s\",\"url\":\"%s\","
458460
"\"source_id\":\"%s\",\"source_name\":\"%s\"}%s\n",
459-
name_pe, filename_e, desc_e, ver_e, url_e,
461+
name_pe, filename_e, desc_e, ver_e, last_upd_e, url_e,
460462
src_id_e, src_name_e,
461463
(pi < count - 1) ? "," : "") != 0) {
462464
break;

0 commit comments

Comments
 (0)