mirror of
https://github.com/uqbar-dao/nectar.git
synced 2024-11-23 03:44:04 +03:00
app_store UI: simplified downloads page
This commit is contained in:
parent
f963c3705d
commit
7b1312b4ac
@ -3,7 +3,7 @@ import useAppsStore from "../store";
|
||||
|
||||
interface MirrorSelectorProps {
|
||||
packageId: string | undefined;
|
||||
onMirrorSelect: (mirror: string) => void;
|
||||
onMirrorSelect: (mirror: string, status: boolean | null | 'http') => void;
|
||||
}
|
||||
|
||||
const MirrorSelector: React.FC<MirrorSelectorProps> = ({ packageId, onMirrorSelect }) => {
|
||||
@ -40,8 +40,10 @@ const MirrorSelector: React.FC<MirrorSelectorProps> = ({ packageId, onMirrorSele
|
||||
}, [packageId, fetchListing, checkMirror]);
|
||||
|
||||
useEffect(() => {
|
||||
onMirrorSelect(selectedMirror);
|
||||
}, [selectedMirror, onMirrorSelect]);
|
||||
if (selectedMirror) {
|
||||
onMirrorSelect(selectedMirror, mirrorStatuses[selectedMirror]);
|
||||
}
|
||||
}, [selectedMirror, mirrorStatuses, onMirrorSelect]);
|
||||
|
||||
const handleMirrorChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const value = e.target.value;
|
||||
|
@ -317,35 +317,220 @@ td {
|
||||
|
||||
/* Download Page */
|
||||
.downloads-page {
|
||||
background-color: light-dark(var(--off-white), var(--off-black));
|
||||
border-radius: var(--border-radius);
|
||||
padding: 1rem;
|
||||
background-color: light-dark(var(--white), var(--maroon));
|
||||
border-radius: 8px;
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.mirror-selection {
|
||||
max-width: 300px;
|
||||
.app-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.app-header h2 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.launch-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 14px;
|
||||
background-color: var(--orange);
|
||||
color: var(--white);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.launch-button:hover {
|
||||
background-color: var(--dark-orange);
|
||||
}
|
||||
|
||||
.version-selector {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.version-selector select {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 2px solid var(--orange);
|
||||
border-radius: 4px;
|
||||
background-color: light-dark(var(--white), var(--tasteful-dark));
|
||||
color: light-dark(var(--off-black), var(--off-white));
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.version-selector select:focus {
|
||||
outline: none;
|
||||
border-color: var(--dark-orange);
|
||||
box-shadow: 0 0 0 3px rgba(255, 79, 0, 0.2);
|
||||
}
|
||||
|
||||
.download-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.download-button,
|
||||
.install-button,
|
||||
.installed-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.download-button {
|
||||
background-color: var(--orange);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.download-button:hover:not(:disabled) {
|
||||
background-color: var(--dark-orange);
|
||||
}
|
||||
|
||||
.install-button {
|
||||
background-color: var(--blue);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.install-button:hover {
|
||||
background-color: color-mix(in srgb, var(--blue) 80%, black);
|
||||
}
|
||||
|
||||
.installed-button {
|
||||
background-color: var(--gray);
|
||||
color: var(--white);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.download-button:disabled,
|
||||
.install-button:disabled,
|
||||
.installed-button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.my-downloads {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.my-downloads>button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
background-color: transparent;
|
||||
color: var(--orange);
|
||||
border: 2px solid var(--orange);
|
||||
padding: 0.5rem 1rem;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
.my-downloads>button:hover {
|
||||
background-color: var(--orange);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.my-downloads table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.my-downloads th,
|
||||
.my-downloads td {
|
||||
padding: 0.5rem;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--gray);
|
||||
}
|
||||
|
||||
.my-downloads td button {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.app-details {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.detail-section {
|
||||
.app-details>button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
background-color: transparent;
|
||||
color: var(--orange);
|
||||
border: 2px solid var(--orange);
|
||||
padding: 0.5rem 1rem;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
.app-details>button:hover {
|
||||
background-color: var(--orange);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.app-details pre {
|
||||
background-color: light-dark(var(--tan), var(--tasteful-dark));
|
||||
border-radius: var(--border-radius);
|
||||
color: light-dark(var(--off-black), var(--off-white));
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.cap-approval-popup {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.cap-approval-content {
|
||||
background-color: light-dark(var(--white), var(--tasteful-dark));
|
||||
color: light-dark(var(--off-black), var(--off-white));
|
||||
padding: 2rem;
|
||||
border-radius: 8px;
|
||||
max-width: 80%;
|
||||
max-height: 80%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.json-display {
|
||||
background-color: light-dark(var(--tan), var(--off-black));
|
||||
color: light-dark(var(--off-black), var(--off-white));
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
background-color: light-dark(var(--off-white), var(--off-black));
|
||||
padding: 0.5rem;
|
||||
border-radius: var(--border-radius);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.approval-buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
/* My Downloads Page */
|
||||
|
@ -1,10 +1,11 @@
|
||||
import React, { useState, useEffect, useCallback, useMemo } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { FaDownload, FaCheck, FaSpinner, FaRocket, FaChevronDown, FaChevronUp, FaTrash } from "react-icons/fa";
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import { FaDownload, FaSpinner, FaChevronDown, FaChevronUp, FaRocket, FaTrash, FaPlay } from "react-icons/fa";
|
||||
import useAppsStore from "../store";
|
||||
import { MirrorSelector } from '../components';
|
||||
|
||||
export default function DownloadPage() {
|
||||
const navigate = useNavigate();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const {
|
||||
listings,
|
||||
@ -15,14 +16,15 @@ export default function DownloadPage() {
|
||||
downloadApp,
|
||||
installApp,
|
||||
removeDownload,
|
||||
clearAllActiveDownloads,
|
||||
checkMirror,
|
||||
} = useAppsStore();
|
||||
|
||||
const [showMetadata, setShowMetadata] = useState(false);
|
||||
const [selectedMirror, setSelectedMirror] = useState<string>("");
|
||||
|
||||
const [selectedVersion, setSelectedVersion] = useState<string>("");
|
||||
const [showMyDownloads, setShowMyDownloads] = useState(false);
|
||||
const [isMirrorOnline, setIsMirrorOnline] = useState<boolean | null>(null);
|
||||
const [showCapApproval, setShowCapApproval] = useState(false);
|
||||
const [selectedVersion, setSelectedVersion] = useState<{ version: string, hash: string } | null>(null);
|
||||
const [manifest, setManifest] = useState<any>(null);
|
||||
|
||||
const app = useMemo(() => listings[id || ""], [listings, id]);
|
||||
@ -31,21 +33,80 @@ export default function DownloadPage() {
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
clearAllActiveDownloads();
|
||||
fetchData(id);
|
||||
}
|
||||
}, [id, fetchData, clearAllActiveDownloads]);
|
||||
}, [id, fetchData]);
|
||||
|
||||
const handleMirrorSelect = useCallback((mirror: string, status: boolean | null | 'http') => {
|
||||
setSelectedMirror(mirror);
|
||||
setIsMirrorOnline(status === 'http' ? true : status);
|
||||
}, []);
|
||||
|
||||
|
||||
const sortedVersions = useMemo(() => {
|
||||
if (!app || !app.metadata?.properties?.code_hashes) return [];
|
||||
return app.metadata.properties.code_hashes
|
||||
.map(([version, hash]) => ({ version, hash }))
|
||||
.sort((a, b) => {
|
||||
const vA = a.version.split('.').map(Number);
|
||||
const vB = b.version.split('.').map(Number);
|
||||
for (let i = 0; i < Math.max(vA.length, vB.length); i++) {
|
||||
if (vA[i] > vB[i]) return -1;
|
||||
if (vA[i] < vB[i]) return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}, [app]);
|
||||
|
||||
useEffect(() => {
|
||||
if (app && !selectedMirror) {
|
||||
setSelectedMirror(app.package_id.publisher_node || "");
|
||||
if (sortedVersions.length > 0 && !selectedVersion) {
|
||||
setSelectedVersion(sortedVersions[0].version);
|
||||
}
|
||||
}, [app, selectedMirror]);
|
||||
}, [sortedVersions, selectedVersion]);
|
||||
|
||||
const handleDownload = useCallback((version: string, hash: string) => {
|
||||
if (!id || !selectedMirror || !app) return;
|
||||
downloadApp(id, hash, selectedMirror);
|
||||
}, [id, selectedMirror, app, downloadApp]);
|
||||
const isDownloaded = useMemo(() => {
|
||||
if (!app || !selectedVersion) return false;
|
||||
const versionData = sortedVersions.find(v => v.version === selectedVersion);
|
||||
if (!versionData) return false;
|
||||
return appDownloads.some(d => d.File && d.File.name === `${versionData.hash}.zip`);
|
||||
}, [app, selectedVersion, sortedVersions, appDownloads]);
|
||||
|
||||
const isDownloading = useMemo(() => {
|
||||
if (!app || !selectedVersion) return false;
|
||||
const versionData = sortedVersions.find(v => v.version === selectedVersion);
|
||||
if (!versionData) return false;
|
||||
const downloadKey = `${app.package_id.package_name}:${selectedMirror}:${versionData.hash}`;
|
||||
return !!activeDownloads[downloadKey];
|
||||
}, [app, selectedVersion, sortedVersions, selectedMirror, activeDownloads]);
|
||||
|
||||
|
||||
const downloadProgress = useMemo(() => {
|
||||
if (!isDownloading || !app || !selectedVersion) return null;
|
||||
const versionData = sortedVersions.find(v => v.version === selectedVersion);
|
||||
if (!versionData) return null;
|
||||
const downloadKey = `${app.package_id.package_name}:${selectedMirror}:${versionData.hash}`;
|
||||
const progress = activeDownloads[downloadKey];
|
||||
return progress ? Math.round((progress.downloaded / progress.total) * 100) : 0;
|
||||
}, [isDownloading, app, selectedVersion, sortedVersions, selectedMirror, activeDownloads]);
|
||||
|
||||
const isCurrentVersionInstalled = useMemo(() => {
|
||||
if (!app || !selectedVersion || !installedApp) return false;
|
||||
const versionData = sortedVersions.find(v => v.version === selectedVersion);
|
||||
return versionData ? installedApp.our_version_hash === versionData.hash : false;
|
||||
}, [app, selectedVersion, installedApp, sortedVersions]);
|
||||
|
||||
const handleDownload = useCallback(() => {
|
||||
if (!id || !selectedMirror || !app || !selectedVersion) return;
|
||||
const versionData = sortedVersions.find(v => v.version === selectedVersion);
|
||||
if (versionData) {
|
||||
downloadApp(id, versionData.hash, selectedMirror);
|
||||
}
|
||||
}, [id, selectedMirror, app, selectedVersion, sortedVersions, downloadApp]);
|
||||
|
||||
const handleRemoveDownload = useCallback((hash: string) => {
|
||||
if (!id) return;
|
||||
removeDownload(id, hash).then(() => fetchData(id));
|
||||
}, [id, removeDownload, fetchData]);
|
||||
|
||||
const handleInstall = useCallback((version: string, hash: string) => {
|
||||
if (!id || !app) return;
|
||||
@ -54,7 +115,6 @@ export default function DownloadPage() {
|
||||
try {
|
||||
const manifestData = JSON.parse(download.File.manifest);
|
||||
setManifest(manifestData);
|
||||
setSelectedVersion({ version, hash });
|
||||
setShowCapApproval(true);
|
||||
} catch (error) {
|
||||
console.error('Failed to parse manifest:', error);
|
||||
@ -64,37 +124,27 @@ export default function DownloadPage() {
|
||||
}
|
||||
}, [id, app, appDownloads]);
|
||||
|
||||
const canDownload = useMemo(() => {
|
||||
return selectedMirror && (isMirrorOnline === true || selectedMirror.startsWith('http')) && !isDownloading && !isDownloaded;
|
||||
}, [selectedMirror, isMirrorOnline, isDownloading, isDownloaded]);
|
||||
|
||||
const confirmInstall = useCallback(() => {
|
||||
if (!id || !selectedVersion) return;
|
||||
installApp(id, selectedVersion.hash).then(() => {
|
||||
fetchData(id);
|
||||
setShowCapApproval(false);
|
||||
setManifest(null);
|
||||
});
|
||||
}, [id, selectedVersion, installApp, fetchData]);
|
||||
const versionData = sortedVersions.find(v => v.version === selectedVersion);
|
||||
if (versionData) {
|
||||
installApp(id, versionData.hash).then(() => {
|
||||
fetchData(id);
|
||||
setShowCapApproval(false);
|
||||
setManifest(null);
|
||||
});
|
||||
}
|
||||
}, [id, selectedVersion, sortedVersions, installApp, fetchData]);
|
||||
|
||||
const handleRemoveDownload = useCallback((version: string, hash: string) => {
|
||||
if (!id) return;
|
||||
removeDownload(id, hash).then(() => fetchData(id));
|
||||
}, [id, removeDownload, fetchData]);
|
||||
|
||||
const versionList = useMemo(() => {
|
||||
if (!app || !app.metadata?.properties?.code_hashes) return [];
|
||||
|
||||
return app.metadata.properties.code_hashes.map(([version, hash]) => {
|
||||
const download = appDownloads.find(d => d.File && d.File.name === `${hash}.zip`);
|
||||
const downloadKey = `${app.package_id.package_name}:${app.package_id.publisher_node}:${hash}`;
|
||||
const activeDownload = activeDownloads[downloadKey];
|
||||
const isDownloaded = !!download?.File && download.File.size > 0;
|
||||
const isInstalled = installedApp?.our_version_hash === hash;
|
||||
const isDownloading = !!activeDownload && activeDownload.downloaded < activeDownload.total;
|
||||
const progress = isDownloading ? activeDownload : { downloaded: 0, total: 100 };
|
||||
|
||||
console.log(`Version ${version} - isInstalled: ${isInstalled}, installedApp:`, installedApp);
|
||||
|
||||
return { version, hash, isDownloaded, isInstalled, isDownloading, progress };
|
||||
});
|
||||
}, [app, appDownloads, activeDownloads, installedApp]);
|
||||
const handleLaunch = useCallback(() => {
|
||||
if (app) {
|
||||
navigate(`/${app.package_id.package_name}:${app.package_id.package_name}:${app.package_id.publisher_node}/`);
|
||||
}
|
||||
}, [app, navigate]);
|
||||
|
||||
if (!app) {
|
||||
return <div className="downloads-page"><h4>Loading app details...</h4></div>;
|
||||
@ -102,82 +152,107 @@ export default function DownloadPage() {
|
||||
|
||||
return (
|
||||
<div className="downloads-page">
|
||||
<h2>{app.metadata?.name || app.package_id.package_name}</h2>
|
||||
<div className="app-header">
|
||||
<h2>{app.metadata?.name || app.package_id.package_name}</h2>
|
||||
{installedApp && (
|
||||
<button onClick={handleLaunch} className="launch-button">
|
||||
<FaPlay /> Launch
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<p>{app.metadata?.description}</p>
|
||||
|
||||
<MirrorSelector packageId={id} onMirrorSelect={setSelectedMirror} />
|
||||
<div className="version-selector">
|
||||
<select
|
||||
value={selectedVersion}
|
||||
onChange={(e) => setSelectedVersion(e.target.value)}
|
||||
>
|
||||
{sortedVersions.map(({ version }, index) => (
|
||||
<option key={version} value={version}>
|
||||
{version} {index === 0 ? "(newest)" : ""}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="version-list">
|
||||
<h3>Available Versions</h3>
|
||||
{versionList.length === 0 ? (
|
||||
<p>No versions available for this app.</p>
|
||||
<div className="download-section">
|
||||
<MirrorSelector
|
||||
packageId={id}
|
||||
onMirrorSelect={handleMirrorSelect}
|
||||
/>
|
||||
{isCurrentVersionInstalled ? (
|
||||
<button className="installed-button" disabled>
|
||||
<FaRocket /> Installed
|
||||
</button>
|
||||
) : isDownloaded ? (
|
||||
<button
|
||||
onClick={() => {
|
||||
const versionData = sortedVersions.find(v => v.version === selectedVersion);
|
||||
if (versionData) {
|
||||
handleInstall(versionData.version, versionData.hash);
|
||||
}
|
||||
}}
|
||||
className="install-button"
|
||||
>
|
||||
<FaRocket /> Install
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={handleDownload}
|
||||
disabled={!canDownload}
|
||||
className="download-button"
|
||||
>
|
||||
{isDownloading ? (
|
||||
<>
|
||||
<FaSpinner className="fa-spin" />
|
||||
Downloading... {downloadProgress}%
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FaDownload /> Download
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="my-downloads">
|
||||
<button onClick={() => setShowMyDownloads(!showMyDownloads)}>
|
||||
{showMyDownloads ? <FaChevronUp /> : <FaChevronDown />} My Downloads
|
||||
</button>
|
||||
{showMyDownloads && (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Version</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{versionList.map(({ version, hash, isDownloaded, isInstalled, isDownloading, progress }) => (
|
||||
<tr key={version}>
|
||||
<td>{version}</td>
|
||||
<td>
|
||||
{isInstalled ? 'Installed' : isDownloaded ? 'Downloaded' : isDownloading ? 'Downloading' : 'Not downloaded'}
|
||||
</td>
|
||||
<td>
|
||||
{!isDownloaded && !isDownloading && (
|
||||
<button
|
||||
onClick={() => handleDownload(version, hash)}
|
||||
disabled={!selectedMirror}
|
||||
className="download-button"
|
||||
>
|
||||
<FaDownload /> Download
|
||||
{appDownloads.map((download) => {
|
||||
const fileName = download.File?.name;
|
||||
const hash = fileName ? fileName.replace('.zip', '') : '';
|
||||
const versionData = sortedVersions.find(v => v.hash === hash);
|
||||
if (!versionData) return null;
|
||||
return (
|
||||
<tr key={hash}>
|
||||
<td>{versionData.version}</td>
|
||||
<td>
|
||||
<button onClick={() => handleInstall(versionData.version, hash)}>
|
||||
<FaRocket /> Install
|
||||
</button>
|
||||
)}
|
||||
{isDownloading && (
|
||||
<div className="download-progress">
|
||||
<FaSpinner className="fa-spin" />
|
||||
Downloading... {Math.round((progress.downloaded / progress.total) * 100)}%
|
||||
</div>
|
||||
)}
|
||||
{isDownloaded && !isInstalled && (
|
||||
<>
|
||||
<button
|
||||
onClick={() => handleInstall(version, hash)}
|
||||
className="install-button"
|
||||
>
|
||||
<FaRocket /> Install
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleRemoveDownload(version, hash)}
|
||||
className="delete-button"
|
||||
>
|
||||
<FaTrash /> Delete
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{isInstalled && <FaCheck className="installed" />}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
<button onClick={() => handleRemoveDownload(hash)}>
|
||||
<FaTrash /> Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="app-details">
|
||||
<h3>App Details</h3>
|
||||
<button onClick={() => setShowMetadata(!showMetadata)}>
|
||||
{showMetadata ? <FaChevronUp /> : <FaChevronDown />} Metadata
|
||||
</button>
|
||||
{showMetadata && (
|
||||
<pre>{JSON.stringify(app.metadata, null, 2)}</pre>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showCapApproval && manifest && (
|
||||
<div className="cap-approval-popup">
|
||||
<div className="cap-approval-content">
|
||||
@ -194,6 +269,17 @@ export default function DownloadPage() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
<div className="app-details">
|
||||
<h3>App Details</h3>
|
||||
<button onClick={() => setShowMetadata(!showMetadata)}>
|
||||
{showMetadata ? <FaChevronUp /> : <FaChevronDown />} Metadata
|
||||
</button>
|
||||
{showMetadata && (
|
||||
<pre>{JSON.stringify(app.metadata, null, 2)}</pre>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue
Block a user