fix(ui): inject dashboard icons and metadata from repository context

This addresses the issue where dashboard payloads lacked icons.

Instead of passing the icon through the C backend's payload list, this leverages the existing `RepositoryContext` remote catalog.

- Wrapped the root `App` in `RepositoryProvider` inside `main.jsx`.

- Added `filenameMap` to `RepositoryContext` to build a fast O(1) lookup map of all payload metadata across sources.

- Injected the map data directly into Dashboard `PayloadButton` cards.
This commit is contained in:
najdek committed 2026-08-10 18:01:08 +02:00
1 parent b1f4fa0044
commit 701ac66f9b
7 files changed
+76 -31

No files matched your search

+18 -3
View File
@@ -47,12 +47,27 @@ Fix applied:
## High
### 3. Dashboard icons are always null
App.jsx passes `icon={payloadMeta[p.split('/').pop()]?.icon}` but
### 3. Dashboard icons are always null — ✅ FIXED
App.jsx passed `icon={payloadMeta[p.split('/').pop()]?.icon}` but
`payload_mgr_list_json` adds only `author` + `details_url` to the meta (per plan), never
`icon`. Every dashboard card renders the fallback generated icon. The planned
`icon`. Every dashboard card rendered the fallback generated icon. The planned
"lifted repo filename→icon map" was never wired into the dashboard.
Fix applied:
- `RepositoryProvider` moved to `main.jsx` (drops the `config` prop dependency) so App can
consume the context via `useRepository()`.
- Context now derives and exposes `filenameMap`: `filename → { icon, author,
release_notes, details_url, display_name }`, built from all payload entries in
`/repository_payloads` (handles both `sources[]` and bare-array `payloads[]` shapes).
- App's dashboard `PayloadButton` now sources `icon`, `author`, and `displayName` from
`filenameMap` (falling back to `payloadMeta` for author, and to filename parsing when no
map entry exists).
- `PayloadName` gained a `displayName` override prop; `PayloadCard` (shop) passes the repo
`name` as well.
- Note: the live mirror (`payloads.json`) currently ships a bare array without
`icon`/`author`/`release_notes` yet (mirror tool out of scope), so dashboard icons still
fall back to generated icons until the mirror adds them — but the wiring is now correct.
### 4. No `SHOW_UPDATE_NOTIFICATIONS` UI toggle
`show_update_notifications` is read/written in config.c (defaults on) and the server-side
gate in `/update_check` works, but SettingsView has no toggle (plan required one).
+9 -9
View File
@@ -19,7 +19,7 @@ import { useTranslation } from 'react-i18next'
import './App.css'
// Contexts
import { RepositoryProvider } from './contexts/RepositoryContext'
import { useRepository } from './contexts/RepositoryContext'
// Utilities
import { cn, isPS5, isSystemPayload } from './utils/helpers'
@@ -44,6 +44,7 @@ import ActiveProcessesView from './components/views/ActiveProcessesView'
function App() {
const { t } = useTranslation();
const { filenameMap } = useRepository()
const [view, setView] = useState('dashboard')
const [isFavoriteEditMode, setIsFavoriteEditMode] = useState(false)
const [isManageMode, setIsManageMode] = useState(false)
@@ -476,11 +477,10 @@ function App() {
}
return (
<RepositoryProvider config={config}>
<div className={cn(
"min-h-screen min-h-[100dvh] ps5-bg text-zinc-100 font-ps5 flex",
isPS5 ? "flex-row overflow-hidden" : "flex-col md:flex-row md:overflow-hidden"
)}>
<div className={cn(
"min-h-screen min-h-[100dvh] ps5-bg text-zinc-100 font-ps5 flex",
isPS5 ? "flex-row overflow-hidden" : "flex-col md:flex-row md:overflow-hidden"
)}>
{/* Toast Container */}
<div className="fixed top-0 right-0 p-8 z-[2000] space-y-4 pointer-events-none">
{toasts.map(t => (
@@ -660,8 +660,9 @@ function App() {
isLoading={loading && activeLoadingName === p.split('/').pop().replace(/\.(elf|bin)$/i, '').replace(/_/g, ' ')}
sourceName={config.MULTI_SOURCES_ENABLED ? (payloadMeta[p.split('/').pop()]?.source_name || null) : null}
version={payloadMeta[p.split('/').pop()]?.version || null}
icon={payloadMeta[p.split('/').pop()]?.icon || null}
author={payloadMeta[p.split('/').pop()]?.author || null}
icon={payloadMeta[p.split('/').pop()]?.icon || filenameMap[p.split('/').pop()]?.icon || null}
author={payloadMeta[p.split('/').pop()]?.author || filenameMap[p.split('/').pop()]?.author || null}
displayName={filenameMap[p.split('/').pop()]?.display_name || null}
isFavorite={favoritePayloads.includes(p)}
isLaunched={launchHistory.includes(p)}
isEditMode={isFavoriteEditMode}
@@ -867,7 +868,6 @@ function App() {
</div>
)}
</div>
</RepositoryProvider>
)
}
+2 -2
View File
@@ -5,7 +5,7 @@ import PayloadName from './PayloadName'
import PayloadIcon from './PayloadIcon'
import { cn } from '../../utils/helpers'
const PayloadButton = ({ path, onClick, isLoading, sourceName, version, icon, author, isFavorite, isLaunched, isEditMode, isManageMode, onMoveFavorite, canMoveLeft, canMoveRight }) => {
const PayloadButton = ({ path, onClick, isLoading, sourceName, version, icon, author, displayName, isFavorite, isLaunched, isEditMode, isManageMode, onMoveFavorite, canMoveLeft, canMoveRight }) => {
const { t } = useTranslation()
const isUsb = path.startsWith('/mnt/usb')
return (
@@ -22,7 +22,7 @@ const PayloadButton = ({ path, onClick, isLoading, sourceName, version, icon, au
<div className="flex items-center gap-4 min-w-0 pr-2">
<PayloadIcon icon={icon} name={path.split('/').pop()} className="w-12 h-12 text-xl" />
<div className="min-w-0 flex-1">
<PayloadName path={path} hideVersion={true} className="text-white text-xl" stacked={false} />
<PayloadName path={path} hideVersion={true} displayName={displayName} className="text-white text-xl" stacked={false} />
<div className="flex items-center gap-2 mt-0.5">
{version && <span className="text-[10px] text-zinc-400 font-bold bg-white/10 px-1.5 py-0.5 rounded uppercase shrink-0">{version}</span>}
{author && (
+1 -1
View File
@@ -16,7 +16,7 @@ const PayloadCard = ({ payload, onClick, isUpdate, isInstalled }) => {
<PayloadIcon icon={payload.icon} name={payload.filename} className="w-20 h-20 md:w-28 md:h-28" />
</div>
<div className="flex flex-col flex-1 min-w-0 w-full text-center">
<PayloadName path={payload.filename} hideVersion={true} centered={true} className="text-lg md:text-xl text-white font-extrabold truncate w-full flex justify-center" />
<PayloadName path={payload.filename} hideVersion={true} centered={true} displayName={payload.name} className="text-lg md:text-xl text-white font-extrabold truncate w-full flex justify-center" />
<div className="flex items-center justify-center gap-2 mt-1 w-full">
{payload.version && <span className="text-[10px] text-zinc-400 font-bold bg-white/10 px-1.5 py-0.5 rounded uppercase shrink-0">{payload.version}</span>}
{payload.author && (
+2 -1
View File
@@ -3,11 +3,12 @@ import { Zap, Usb } from 'lucide-react'
import { cn, parsePayloadName } from '../../utils/helpers'
import { useTranslation } from 'react-i18next'
const PayloadName = ({ path, version: repoVersion, className, versionClassName, stacked = false, hideIcon = false, lastUpdate = null, hideVersion = false, centered = false }) => {
const PayloadName = ({ path, version: repoVersion, className, versionClassName, stacked = false, hideIcon = false, lastUpdate = null, hideVersion = false, centered = false, displayName: metaDisplayName = null }) => {
const { t } = useTranslation();
let { displayName, version, isDelay, delaySeconds } = parsePayloadName(path);
const isUsb = path?.startsWith('/mnt/usb');
if (metaDisplayName) displayName = metaDisplayName;
if (repoVersion) version = repoVersion;
if (hideVersion) version = null;
if (isDelay) {
+40 -14
View File
@@ -1,17 +1,14 @@
import React, { createContext, useContext, useState, useEffect, useCallback } from 'react';
import React, { createContext, useContext, useState, useEffect, useCallback, useMemo } from 'react';
const RepositoryContext = createContext();
export const useRepository = () => useContext(RepositoryContext);
export const RepositoryProvider = ({ children, config }) => {
export const RepositoryProvider = ({ children }) => {
const [repoData, setRepoData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
// Safely check if config exists before checking MULTI_SOURCES_ENABLED
const multiSources = config?.MULTI_SOURCES_ENABLED === true;
const fetchRemote = useCallback(async (force = false) => {
setLoading(true);
setError(false);
@@ -20,7 +17,7 @@ export const RepositoryProvider = ({ children, config }) => {
const res = await fetch(endpoint);
if (!res.ok) throw new Error();
const data = await res.json();
setRepoData(data);
} catch (err) {
console.error(err);
@@ -29,19 +26,48 @@ export const RepositoryProvider = ({ children, config }) => {
} finally {
setLoading(false);
}
}, [multiSources]);
}, []);
// Fetch only once when config is loaded
// Fetch only once on mount
useEffect(() => {
if (config && Object.keys(config).length > 0) {
if (repoData === null) {
fetchRemote();
}
if (repoData === null) {
fetchRemote();
}
}, [config, fetchRemote, repoData]);
}, [fetchRemote, repoData]);
// filename -> { icon, author, release_notes, details_url, display_name }
// built from all payload entries in the fetched repository data.
const filenameMap = useMemo(() => {
const map = {};
if (!repoData) return map;
const collect = (entries) => {
if (!Array.isArray(entries)) return;
for (const e of entries) {
if (e && e.filename) {
map[e.filename] = {
icon: e.icon || null,
author: e.author || null,
release_notes: e.release_notes || null,
details_url: e.details_url || null,
display_name: e.name || null,
};
}
}
};
if (Array.isArray(repoData.sources)) {
repoData.sources.forEach((src) => collect(src && src.payloads));
} else if (Array.isArray(repoData.payloads)) {
collect(repoData.payloads);
} else if (repoData.payloads && Array.isArray(repoData.payloads.payloads)) {
collect(repoData.payloads.payloads);
}
return map;
}, [repoData]);
return (
<RepositoryContext.Provider value={{ repoData, loading, error, fetchRemote }}>
<RepositoryContext.Provider value={{ repoData, loading, error, fetchRemote, filenameMap }}>
{children}
</RepositoryContext.Provider>
);
+4 -1
View File
@@ -2,11 +2,14 @@ import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
import { RepositoryProvider } from './contexts/RepositoryContext'
import './i18n';
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
<RepositoryProvider>
<App />
</RepositoryProvider>
</StrictMode>,
)