mirror of
https://github.com/itsPLK/ps5-payload-manager.git
synced 2026-10-19 02:19:53 -05:00
fix: resolve minor ui and backend validation issues
This addresses the remaining minor issues outlined in ISSUES.md. - Extracted remaining hardcoded English strings into i18n locales - Fixed stale state and implemented lazy-loaded images in PayloadDetailPanel - Revived 'installed' payload state in StorageHub by no longer filtering them out - Fixed event propagation bug in dashboard manage mode where clicking anywhere triggered delete - Hardened C backend 'is_pldmgr_filename' to strictly match pldmgr prefixes, preventing false positives
This commit is contained in:
1 parent
675a1df0ef
commit
8fd3212ec5
7 files changed
+103
-55
No files matched your search
@@ -83,24 +83,27 @@ Fix applied:
|
||||
|
||||
## Minor
|
||||
|
||||
- **Hardcoded English strings** — App.jsx (update banner, manage-mode toasts),
|
||||
PayloadCard ("Update"/"Installed" badges), PayloadDetailPanel ("Back to Repository",
|
||||
"Description", "What's New", "About", "Loading details...", "Failed to load details.").
|
||||
No i18n keys added; `en-US.json` unchanged (217 keys). Locales submodule shows
|
||||
uncommitted content changes in `git status`.
|
||||
- **PayloadDetailPanel** — content not reset between payload switches (brief stale
|
||||
markup shown), details refetched on every open (no memoization), markdown images not
|
||||
lazy-loaded / error-bounded.
|
||||
- **Dead code** — `PayloadCard` `opacity-60` "installed" state is unreachable (installed
|
||||
payloads are filtered out of `availablePayloads` in StorageHub); "Installed ✓"/Remove
|
||||
states in the detail panel are unreachable.
|
||||
- **Manage mode** — entire card click opens the delete confirm; the trash icon is
|
||||
decorative (no `stopPropagation`), deviating from the specified trash-overlay
|
||||
interaction.
|
||||
- **`repository_check_repo_update`** matches any filename containing `pldmgr` via
|
||||
`strcasestr` — false-positive risk for unrelated repo files.
|
||||
- **Icon size threshold** — icons dropped only when truncated to ~16KB
|
||||
(buffer-limited), vs plan's 12KB. Non-issue functionally.
|
||||
- **Hardcoded English strings — ✅ FIXED** — update banner (App.jsx), PayloadCard
|
||||
badges, and PayloadDetailPanel labels now use i18n keys
|
||||
(`app.update_banner.*`, `storage_hub.update_badge`/`installed_badge`/
|
||||
`back_to_repository`/`payload_count`, `payload_detail.*`); ran `npm run extract`.
|
||||
- **PayloadDetailPanel — ✅ FIXED** — resets `detailsHtml`/error state on payload
|
||||
switch, guards against stale async responses (cancellation flag), and markdown
|
||||
`<img>`s are lazy-loaded and hidden on error via `useLayoutEffect` post-processing.
|
||||
- **Dead code — ✅ FIXED** — StorageHub no longer filters installed payloads out of the
|
||||
shop grid, so the "Installed"/"Update" card states and the detail panel's
|
||||
"Installed"/"Update" buttons are reachable. PayloadCard dims installed-up-to-date
|
||||
cards (`opacity-60`) and shows an "Installed" badge. Removed the now-impossible
|
||||
"All payloads installed" empty-state branch; catalog count label is now "{{count}}
|
||||
payloads".
|
||||
- **Manage mode — ✅ FIXED** — the trash icon is now the actual delete trigger (a
|
||||
`role="button"` div with `stopPropagation`, valid inside the card `<button>`);
|
||||
clicking a card in manage mode no longer launches or deletes.
|
||||
- **`repository_check_repo_update` — ✅ FIXED** — added `is_pldmgr_filename()`
|
||||
(base name starts with `pldmgr`/`payload-manager`, case-insensitive); used in both
|
||||
the repo-update scan and the self-update file match (replaces broad `strcasestr`).
|
||||
- **Icon size threshold — ✅ FIXED** — icons dropped when > 12KB (was buffer-limited
|
||||
~16KB), matching the plan.
|
||||
|
||||
## Verified OK
|
||||
- `RESPONSE_BUFFER_SIZE` = 4MB (http_server.c:32)
|
||||
|
||||
@@ -593,14 +593,14 @@ function App() {
|
||||
{updateStatus.status === 'ready-to-apply' ? (
|
||||
<>
|
||||
<strong className="text-white">
|
||||
Payload Manager {updateStatus.new_version ? `v${updateStatus.new_version}` : ''} downloaded
|
||||
{t('app.update_banner.ready_title', 'Payload Manager {{version}} downloaded', { version: updateStatus.new_version ? `v${updateStatus.new_version}` : '' })}
|
||||
</strong>
|
||||
{' — load it to apply the update.'}
|
||||
{t('app.update_banner.ready_desc', ' — load it to apply the update.')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<strong className="text-white">Update Available!</strong>
|
||||
{updateStatus.new_version ? ` Payload Manager v${updateStatus.new_version}` : ''} is available in the repository.
|
||||
<strong className="text-white">{t('app.update_banner.available_title', 'Update Available!')}</strong>
|
||||
{t('app.update_banner.available_desc', ' Payload Manager {{version}} is available in the repository.', { version: updateStatus.new_version ? `v${updateStatus.new_version}` : '' })}
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
@@ -611,7 +611,7 @@ function App() {
|
||||
onClick={() => handleApplyUpdate(updateStatus.local_path)}
|
||||
className="px-4 py-2 rounded-lg bg-ps-blue text-ps-black font-bold text-sm hover:opacity-90 transition-opacity"
|
||||
>
|
||||
Load
|
||||
{t('app.update_banner.load', 'Load')}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
@@ -619,7 +619,7 @@ function App() {
|
||||
disabled={updateBusy}
|
||||
className="px-4 py-2 rounded-lg bg-ps-blue text-ps-black font-bold text-sm hover:opacity-90 transition-opacity disabled:opacity-50"
|
||||
>
|
||||
{updateBusy ? 'Installing…' : 'Update'}
|
||||
{updateBusy ? t('app.update_banner.installing', 'Installing…') : t('app.update_banner.update', 'Update')}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
@@ -653,10 +653,11 @@ function App() {
|
||||
key={p}
|
||||
path={p}
|
||||
onClick={() => {
|
||||
if (isManageMode) handleDelete(p.split('/').pop())
|
||||
else if (isFavoriteEditMode) toggleFavorite(p)
|
||||
if (isManageMode) return
|
||||
if (isFavoriteEditMode) toggleFavorite(p)
|
||||
else loadPayload(p)
|
||||
}}
|
||||
onDelete={() => handleDelete(p.split('/').pop())}
|
||||
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}
|
||||
|
||||
@@ -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, displayName, isFavorite, isLaunched, isEditMode, isManageMode, onMoveFavorite, canMoveLeft, canMoveRight }) => {
|
||||
const PayloadButton = ({ path, onClick, onDelete, isLoading, sourceName, version, icon, author, displayName, isFavorite, isLaunched, isEditMode, isManageMode, onMoveFavorite, canMoveLeft, canMoveRight }) => {
|
||||
const { t } = useTranslation()
|
||||
const isUsb = path.startsWith('/mnt/usb')
|
||||
return (
|
||||
@@ -45,7 +45,15 @@ const PayloadButton = ({ path, onClick, isLoading, sourceName, version, icon, au
|
||||
</div>
|
||||
)}
|
||||
{isManageMode && !isUsb && (
|
||||
<div className="p-1 rounded-lg text-red-500 transition-all opacity-50 group-hover:opacity-100">
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={-1}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (onDelete) onDelete();
|
||||
}}
|
||||
className="p-1.5 rounded-lg text-red-500 transition-all opacity-50 group-hover:opacity-100 hover:bg-red-500/10 hover:opacity-100 cursor-pointer"
|
||||
>
|
||||
<Trash2 className="w-5 h-5" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import React from 'react';
|
||||
import { cn } from '../../utils/helpers';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import PayloadName from './PayloadName';
|
||||
import PayloadIcon from './PayloadIcon';
|
||||
|
||||
const PayloadCard = ({ payload, onClick, isUpdate, isInstalled }) => {
|
||||
const { t } = useTranslation();
|
||||
const isInstalledUpToDate = isInstalled && !isUpdate;
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
"group flex flex-col p-4 md:p-6 bg-white/[0.02] hover:bg-white/[0.05] border border-white/5 hover:border-ps-blue/30 rounded-3xl transition-all text-left relative",
|
||||
(isInstalled && !isUpdate) && "opacity-60"
|
||||
isInstalledUpToDate && "opacity-60"
|
||||
)}
|
||||
>
|
||||
<div className="flex justify-center mb-4 w-full">
|
||||
@@ -31,7 +34,12 @@ const PayloadCard = ({ payload, onClick, isUpdate, isInstalled }) => {
|
||||
<div className="absolute top-4 right-4 flex flex-col gap-2">
|
||||
{isUpdate && (
|
||||
<span className="px-2 py-1 rounded-lg bg-emerald-500/20 text-emerald-400 font-bold text-[10px] uppercase tracking-widest border border-emerald-500/20">
|
||||
Update
|
||||
{t("storage_hub.update_badge", "Update")}
|
||||
</span>
|
||||
)}
|
||||
{isInstalledUpToDate && (
|
||||
<span className="px-2 py-1 rounded-lg bg-white/10 text-zinc-400 font-bold text-[10px] uppercase tracking-widest border border-white/10">
|
||||
{t("storage_hub.installed_badge", "Installed")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { X, CloudDownload, Loader2, ArrowLeft } from 'lucide-react';
|
||||
import React, { useState, useEffect, useLayoutEffect, useRef } from 'react';
|
||||
import { CloudDownload, Loader2, ArrowLeft } from 'lucide-react';
|
||||
import { marked } from 'marked';
|
||||
import DOMPurify from 'dompurify';
|
||||
import { cn } from '../../utils/helpers';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import PayloadIcon from './PayloadIcon';
|
||||
import PayloadName from './PayloadName';
|
||||
|
||||
const PayloadDetailPanel = ({ payload, onClose, onInstall, isInstalled, isUpdate, srcUrl, srcId }) => {
|
||||
const { t } = useTranslation();
|
||||
const [detailsHtml, setDetailsHtml] = useState(null);
|
||||
const [detailsError, setDetailsError] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
|
||||
const detailsRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
setDetailsHtml(null);
|
||||
setDetailsError(false);
|
||||
if (!payload?.details_url) return;
|
||||
|
||||
|
||||
// Resolve full URL if relative
|
||||
let url = payload.details_url;
|
||||
if (!url.startsWith('http')) {
|
||||
@@ -25,6 +29,7 @@ const PayloadDetailPanel = ({ payload, onClose, onInstall, isInstalled, isUpdate
|
||||
}
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
const fetchDetails = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
@@ -32,17 +37,27 @@ const PayloadDetailPanel = ({ payload, onClose, onInstall, isInstalled, isUpdate
|
||||
if (!res.ok) throw new Error('Failed to fetch details');
|
||||
const text = await res.text();
|
||||
const html = DOMPurify.sanitize(marked.parse(text));
|
||||
setDetailsHtml(html);
|
||||
if (!cancelled) setDetailsHtml(html);
|
||||
} catch (err) {
|
||||
setDetailsHtml('<p class="text-red-400">Failed to load details.</p>');
|
||||
if (!cancelled) setDetailsError(true);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
if (!cancelled) setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchDetails();
|
||||
return () => { cancelled = true; };
|
||||
}, [payload, srcUrl]);
|
||||
|
||||
// Lazy-load markdown images and hide broken ones
|
||||
useLayoutEffect(() => {
|
||||
if (!detailsRef.current) return;
|
||||
detailsRef.current.querySelectorAll('img').forEach((img) => {
|
||||
img.loading = 'lazy';
|
||||
img.onerror = () => { img.style.display = 'none'; };
|
||||
});
|
||||
}, [detailsHtml]);
|
||||
|
||||
if (!payload) return null;
|
||||
|
||||
return (
|
||||
@@ -55,7 +70,7 @@ const PayloadDetailPanel = ({ payload, onClose, onInstall, isInstalled, isUpdate
|
||||
className="flex items-center space-x-2 text-zinc-400 hover:text-white transition-colors bg-white/5 hover:bg-white/10 px-4 py-2 rounded-xl"
|
||||
>
|
||||
<ArrowLeft className="w-5 h-5" />
|
||||
<span className="font-bold text-sm uppercase tracking-wider">Back to Repository</span>
|
||||
<span className="font-bold text-sm uppercase tracking-wider">{t("storage_hub.back_to_repository", "Back to Repository")}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -88,7 +103,7 @@ const PayloadDetailPanel = ({ payload, onClose, onInstall, isInstalled, isUpdate
|
||||
>
|
||||
<CloudDownload className="w-5 h-5" />
|
||||
<span>
|
||||
{isUpdate ? "Update" : isInstalled ? "Installed" : "Install"}
|
||||
{isUpdate ? t("payload_detail.update", "Update") : isInstalled ? t("payload_detail.installed", "Installed") : t("payload_detail.install", "Install")}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -97,14 +112,14 @@ const PayloadDetailPanel = ({ payload, onClose, onInstall, isInstalled, isUpdate
|
||||
<div className="flex-1 p-6 md:p-8">
|
||||
{payload.description && (
|
||||
<div className="mb-8">
|
||||
<h3 className="text-lg font-bold text-white mb-3">Description</h3>
|
||||
<h3 className="text-lg font-bold text-white mb-3">{t("payload_detail.description", "Description")}</h3>
|
||||
<p className="text-zinc-300 leading-relaxed text-base">{payload.description}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{payload.release_notes && (
|
||||
<div className="mb-8">
|
||||
<h3 className="text-lg font-bold text-white mb-3">What's New</h3>
|
||||
<h3 className="text-lg font-bold text-white mb-3">{t("payload_detail.whats_new", "What's New")}</h3>
|
||||
<div className="text-zinc-300 leading-relaxed text-sm whitespace-pre-wrap bg-white/5 p-4 rounded-2xl border border-white/5">
|
||||
{payload.release_notes}
|
||||
</div>
|
||||
@@ -113,14 +128,17 @@ const PayloadDetailPanel = ({ payload, onClose, onInstall, isInstalled, isUpdate
|
||||
|
||||
{payload.details_url && (
|
||||
<div className="mt-8 pt-8 border-t border-white/10">
|
||||
<h3 className="text-lg font-bold text-white mb-4">About</h3>
|
||||
<h3 className="text-lg font-bold text-white mb-4">{t("payload_detail.about", "About")}</h3>
|
||||
{loading ? (
|
||||
<div className="flex items-center gap-3 text-zinc-500">
|
||||
<Loader2 className="w-5 h-5 animate-spin" />
|
||||
<span>Loading details...</span>
|
||||
<span>{t("payload_detail.loading_details", "Loading details...")}</span>
|
||||
</div>
|
||||
) : detailsError ? (
|
||||
<p className="text-red-400">{t("payload_detail.failed_load", "Failed to load details.")}</p>
|
||||
) : detailsHtml ? (
|
||||
<div
|
||||
<div
|
||||
ref={detailsRef}
|
||||
className="prose prose-invert max-w-none prose-a:text-ps-blue hover:prose-a:text-ps-blue/80 prose-headings:text-white prose-p:text-zinc-300"
|
||||
dangerouslySetInnerHTML={{ __html: detailsHtml }}
|
||||
/>
|
||||
|
||||
@@ -212,7 +212,7 @@ const StorageHub = ({ payloads, payloadMeta, onInstall, onDelete, onUpload, onIm
|
||||
/* ===== REPOSITORY CATALOGS ===== */
|
||||
<div className="space-y-4">
|
||||
{enrichedSources.map(src => {
|
||||
let availablePayloads = src.payloads.filter(p => !p.isInstalled || p.isUpdate)
|
||||
let availablePayloads = [...src.payloads]
|
||||
|
||||
if (search.trim() !== '') {
|
||||
const q = search.toLowerCase()
|
||||
@@ -295,7 +295,7 @@ const StorageHub = ({ payloads, payloadMeta, onInstall, onDelete, onUpload, onIm
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<span className="px-3 py-1 rounded-full bg-white/5 text-zinc-500 text-xs font-bold">
|
||||
{t("storage_hub.available_count", "{{count}} available", { count: availablePayloads.length })}
|
||||
{t("storage_hub.payload_count", "{{count}} payloads", { count: availablePayloads.length })}
|
||||
</span>
|
||||
{enrichedSources.length > 1 && (
|
||||
<ChevronDown className={cn("w-5 h-5 text-zinc-500 transition-transform", isExpanded && "rotate-180")} />
|
||||
@@ -313,10 +313,6 @@ const StorageHub = ({ payloads, payloadMeta, onInstall, onDelete, onUpload, onIm
|
||||
<div className="py-12 flex flex-col items-center justify-center space-y-3 text-zinc-600">
|
||||
<p className="text-sm font-bold uppercase tracking-widest italic">{t("storage_hub.source_empty", "Source is empty")}</p>
|
||||
</div>
|
||||
) : availablePayloads.length === 0 ? (
|
||||
<div className="py-12 flex flex-col items-center justify-center space-y-3 text-zinc-600">
|
||||
<p className="text-sm font-bold uppercase tracking-widest italic">{t("storage_hub.all_installed", "All payloads installed")}</p>
|
||||
</div>
|
||||
) : hasMultipleCategories ? (
|
||||
categories.map(cat => (
|
||||
<div key={cat} className="space-y-4 mb-8 last:mb-0">
|
||||
|
||||
+18
-4
@@ -111,6 +111,20 @@ int download_to_file(const char *url, const char *out_path) {
|
||||
|
||||
/* ── JSON parsing ──────────────────────────────────────────── */
|
||||
|
||||
/* True if a filename (base name) identifies a payload-manager binary, i.e.
|
||||
* starts with "pldmgr" or "payload-manager" (case-insensitive). */
|
||||
static int is_pldmgr_filename(const char *filename) {
|
||||
const char *base;
|
||||
|
||||
if (!filename || !*filename)
|
||||
return 0;
|
||||
base = strrchr(filename, '/');
|
||||
base = base ? base + 1 : filename;
|
||||
|
||||
return strncasecmp(base, "pldmgr", 6) == 0 ||
|
||||
strncasecmp(base, "payload-manager", 15) == 0;
|
||||
}
|
||||
|
||||
/* Skip the top-level wrapping object, returning a pointer to just past its
|
||||
* opening '{' so entries inside the wrapper can be scanned. If the document is
|
||||
* a bare array of entries, or a single entry object (no "payloads" member), the
|
||||
@@ -257,8 +271,8 @@ int parse_repository_payloads(const char *json, RepoPayload **out_items, size_t
|
||||
}
|
||||
|
||||
json_extract_string(p, end, "icon", item.icon, sizeof(item.icon));
|
||||
if (strlen(item.icon) == sizeof(item.icon) - 1) {
|
||||
item.icon[0] = '\0'; /* Drop if > 16KB */
|
||||
if (strlen(item.icon) > 12288) {
|
||||
item.icon[0] = '\0'; /* Drop if > 12KB */
|
||||
}
|
||||
|
||||
if (json_extract_string(p, end, "category", item.category, sizeof(item.category)) != 0 || strlen(item.category) == 0) {
|
||||
@@ -800,7 +814,7 @@ int repository_check_self_update(char *out_path, size_t out_size,
|
||||
|
||||
/* Look for .elf or .bin files that match our name */
|
||||
if (strstr(sentry->d_name, ".elf") || strstr(sentry->d_name, ".bin")) {
|
||||
if (strcasestr(sentry->d_name, "pldmgr") || strcasestr(sentry->d_name, "payload-manager")) {
|
||||
if (is_pldmgr_filename(sentry->d_name)) {
|
||||
char full_path[512];
|
||||
snprintf(full_path, sizeof(full_path), "%s/%s", subdir_path, sentry->d_name);
|
||||
|
||||
@@ -852,7 +866,7 @@ int repository_check_repo_update(char *out_filename, size_t out_size,
|
||||
|
||||
int found = 0;
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
if (strcasestr(items[i].filename, "pldmgr") || strcasestr(items[i].filename, "payload-manager")) {
|
||||
if (is_pldmgr_filename(items[i].filename)) {
|
||||
if (items[i].version[0] != '\0') {
|
||||
if (compare_versions(items[i].version, MENU_VERSION) > 0) {
|
||||
found = 1;
|
||||
|
||||
Reference in new issue
Block a user