mirror of
https://github.com/itsPLK/ps5-payload-manager.git
synced 2026-10-19 02:19:53 -05:00
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:
1 parent
b1f4fa0044
commit
701ac66f9b
7 files changed
+76
-31
No files matched your search
@@ -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).
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>,
|
||||
)
|
||||
|
||||
|
||||
Reference in new issue
Block a user