app_store UI: simplify css

This commit is contained in:
bitful-pannul 2024-08-15 20:42:50 +03:00
parent 59cfbaa59b
commit 9524bb3c5a
11 changed files with 469 additions and 1007 deletions

View File

@ -566,7 +566,7 @@ fn serve_paths(
));
};
let downloads = Address::from_str("downloads@downloads:app_store:sys")?;
let downloads = Address::from_str("our@downloads:app_store:sys")?;
match method {
// start mirroring an app

View File

@ -8,7 +8,6 @@ import StorePage from "./pages/StorePage";
import AppPage from "./pages/AppPage";
import DownloadPage from "./pages/DownloadPage";
import PublishPage from "./pages/PublishPage";
import Testing from "./pages/Testing";
import MyDownloadsPage from "./pages/MyDownloadsPage";
@ -21,9 +20,7 @@ function App() {
<div>
<Router basename={BASE_URL}>
<Header />
<button onClick={() => window.location.href = `${BASE_URL}/testing`}>Go to Testing</button>
<Routes>
<Route path="/testing" element={<Testing />} />
<Route path={STORE_PATH} element={<StorePage />} />
<Route path={MY_DOWNLOADS_PATH} element={<MyDownloadsPage />} />
<Route path={`${APP_DETAILS_PATH}/:id`} element={<AppPage />} />

File diff suppressed because it is too large Load Diff

View File

@ -3,56 +3,82 @@ import { useNavigate, useParams } from "react-router-dom";
import { FaDownload, FaCheck, FaTimes, FaPlay } from "react-icons/fa";
import useAppsStore from "../store";
import { AppListing, PackageState } from "../types/Apps";
import { compareVersions } from "../utils/compareVersions";
export default function AppPage() {
const { id } = useParams();
const navigate = useNavigate();
const { listings, installed, fetchListings, fetchInstalled } = useAppsStore();
const { fetchListing, fetchInstalledApp, installApp } = useAppsStore();
const [app, setApp] = useState<AppListing | null>(null);
const [installedApp, setInstalledApp] = useState<PackageState | null>(null);
const [currentVersion, setCurrentVersion] = useState<string | null>(null);
const [latestVersion, setLatestVersion] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const loadData = useCallback(async () => {
await Promise.all([fetchListings(), fetchInstalled()]);
if (!id) return;
setIsLoading(true);
setError(null);
const foundApp = listings.find(a => `${a.package_id.package_name}:${a.package_id.publisher_node}` === id) || null;
setApp(foundApp);
try {
const [appData, installedAppData] = await Promise.all([
fetchListing(id),
fetchInstalledApp(id)
]);
if (foundApp) {
const foundInstalledApp = installed.find(i =>
i.package_id.package_name === foundApp.package_id.package_name &&
i.package_id.publisher_node === foundApp.package_id.publisher_node
) || null;
setInstalledApp(foundInstalledApp);
setApp(appData);
setInstalledApp(installedAppData);
if (foundApp.metadata?.properties?.code_hashes) {
const versions = foundApp.metadata.properties.code_hashes;
if (appData.metadata?.properties?.code_hashes) {
const versions = appData.metadata.properties.code_hashes;
if (versions.length > 0) {
setLatestVersion(versions[versions.length - 1][0]);
if (foundInstalledApp) {
const installedVersion = versions.find(([_, hash]) => hash === foundInstalledApp.our_version_hash);
const latestVer = versions.reduce((latest, current) =>
compareVersions(current[0], latest[0]) > 0 ? current : latest
)[0];
setLatestVersion(latestVer);
if (installedAppData) {
const installedVersion = versions.find(([_, hash]) => hash === installedAppData.our_version_hash);
if (installedVersion) {
setCurrentVersion(installedVersion[0]);
}
}
}
}
} catch (err) {
setError("Failed to load app details. Please try again.");
console.error(err);
} finally {
setIsLoading(false);
}
}, [id, fetchListings, fetchInstalled]);
}, [id, fetchListing, fetchInstalledApp]);
useEffect(() => {
loadData();
}, [loadData]);
if (!app) {
return <div className="app-page"><h4>App details not found for {id}</h4></div>;
}
const handleDownload = () => {
navigate(`/download/${id}`);
};
const handleLaunch = () => {
// Implement launch functionality
console.log("Launching app:", app?.package_id.package_name);
};
if (isLoading) {
return <div className="app-page"><h4>Loading app details...</h4></div>;
}
if (error) {
return <div className="app-page"><h4>{error}</h4></div>;
}
if (!app) {
return <div className="app-page"><h4>App details not found for {id}</h4></div>;
}
return (
<section className="app-page">
<div className="app-header">
@ -89,7 +115,7 @@ export default function AppPage() {
<FaDownload /> Download
</button>
{installedApp && (
<button className="primary">
<button onClick={handleLaunch} className="primary">
<FaPlay /> Launch
</button>
)}

View File

@ -2,14 +2,14 @@ import React, { useState, useEffect } from "react";
import { useParams } from "react-router-dom";
import { FaDownload, FaCheck, FaSpinner, FaRocket, FaChevronDown, FaChevronUp, FaExclamationTriangle } from "react-icons/fa";
import useAppsStore from "../store";
import { AppListing, DownloadItem, MirrorCheckFile, PackageManifest } from "../types/Apps";
import { DownloadItem, PackageManifest, AppListing } from "../types/Apps";
export default function DownloadPage() {
const { id } = useParams();
const { listings, fetchListings, fetchDownloadsForApp, downloadApp, installApp, checkMirror, fetchInstalled, installed, getCaps, approveCaps } = useAppsStore();
const [downloads, setDownloads] = useState<DownloadItem[]>([]);
const [selectedMirror, setSelectedMirror] = useState<string | null>(null);
const [mirrorStatuses, setMirrorStatuses] = useState<{ [mirror: string]: MirrorCheckFile | null }>({});
const [mirrorStatuses, setMirrorStatuses] = useState<{ [mirror: string]: { status: 'unchecked' | 'checking' | 'online' | 'offline' } }>({});
const [isDownloading, setIsDownloading] = useState(false);
const [isInstalling, setIsInstalling] = useState(false);
const [error, setError] = useState<string | null>(null);
@ -19,8 +19,10 @@ export default function DownloadPage() {
const [manifest, setManifest] = useState<PackageManifest | null>(null);
const [showCapApproval, setShowCapApproval] = useState(false);
const [selectedVersion, setSelectedVersion] = useState<string | null>(null);
const [showManualMirror, setShowManualMirror] = useState(false);
const [manualMirror, setManualMirror] = useState("");
const app = listings.find(a => `${a.package_id.package_name}:${a.package_id.publisher_node}` === id);
const app = listings[id as string];
useEffect(() => {
fetchListings();
@ -32,20 +34,30 @@ export default function DownloadPage() {
useEffect(() => {
if (app) {
checkMirrors();
initializeMirrors();
}
}, [app]);
const checkMirrors = async () => {
const initializeMirrors = () => {
if (!app) return;
const mirrors = [app.package_id.publisher_node, ...(app.metadata?.properties?.mirrors || [])];
const statuses: { [mirror: string]: MirrorCheckFile | null } = {};
for (const mirror of mirrors) {
const initialStatuses: { [mirror: string]: { status: 'unchecked' | 'checking' | 'online' | 'offline' } } = {};
mirrors.forEach(mirror => {
initialStatuses[mirror] = { status: 'unchecked' };
});
setMirrorStatuses(initialStatuses);
setSelectedMirror(app.package_id.publisher_node);
mirrors.forEach(checkMirrorStatus);
};
const checkMirrorStatus = async (mirror: string) => {
setMirrorStatuses(prev => ({ ...prev, [mirror]: { status: 'checking' } }));
try {
const status = await checkMirror(mirror);
statuses[mirror] = status;
setMirrorStatuses(prev => ({ ...prev, [mirror]: { status: status.is_online ? 'online' : 'offline' } }));
} catch (error) {
setMirrorStatuses(prev => ({ ...prev, [mirror]: { status: 'offline' } }));
}
setMirrorStatuses(statuses);
setSelectedMirror(statuses[app.package_id.publisher_node]?.is_online ? app.package_id.publisher_node : mirrors.find(m => statuses[m]?.is_online) || null);
};
const handleDownload = async (version: string) => {
@ -115,25 +127,64 @@ export default function DownloadPage() {
<div className="mirror-selection">
<h3>Select Mirror</h3>
<select
value={selectedMirror || ''}
onChange={(e) => setSelectedMirror(e.target.value)}
value={selectedMirror === manualMirror ? 'manual' : selectedMirror || ''}
onChange={(e) => {
if (e.target.value === 'manual') {
setShowManualMirror(true);
} else {
setSelectedMirror(e.target.value);
setShowManualMirror(false);
setManualMirror('');
}
}}
disabled={isDownloading}
>
<option value="" disabled>Select Mirror</option>
{Object.entries(mirrorStatuses).map(([mirror, status]) => (
<option key={mirror} value={mirror} disabled={!status?.is_online}>
{mirror} {status?.is_online ? '(Online)' : '(Offline)'}
{Object.entries(mirrorStatuses).map(([mirror, { status }]) => (
<option key={mirror} value={mirror} disabled={status === 'offline'}>
{mirror} {status === 'checking' ? '(Checking...)' : status === 'online' ? '(Online)' : status === 'offline' ? '(Offline)' : ''}
</option>
))}
<option value="manual">Manual Mirror (Advanced)</option>
</select>
{(showManualMirror || selectedMirror === manualMirror) && (
<div className="manual-mirror-input">
<input
type="text"
value={manualMirror}
onChange={(e) => setManualMirror(e.target.value)}
placeholder="Enter mirror node"
/>
<button
onClick={() => {
if (manualMirror) {
setSelectedMirror(manualMirror);
setShowManualMirror(true);
}
}}
disabled={!manualMirror}
>
Set Manual Mirror
</button>
<button
onClick={() => {
setSelectedMirror(app.package_id.publisher_node);
setShowManualMirror(false);
setManualMirror('');
}}
>
Reset to Default Mirror
</button>
</div>
)}
</div>
<h3>Available Versions</h3>
<table className="downloads-table">
<thead>
<tr>
<th>Version</th>
<th>Size</th>
<th>Status</th>
<th>Actions</th>
</tr>
@ -147,12 +198,11 @@ export default function DownloadPage() {
return false;
});
const isDownloaded = !!download;
const isInstalled = installed.some(i => i.package_id.package_name === app.package_id.package_name && i.our_version_hash === version);
const isInstalled = installed[id as string]?.our_version_hash === hash;
return (
<tr key={version}>
<td>{version}</td>
<td>{download?.File?.size ? `${(download.File.size / 1024 / 1024).toFixed(2)} MB` : 'N/A'}</td>
<td>
{isInstalled ? 'Installed' : isDownloaded ? 'Downloaded' : 'Not downloaded'}
</td>
@ -160,7 +210,7 @@ export default function DownloadPage() {
{!isDownloaded && (
<button
onClick={() => handleDownload(version)}
disabled={!selectedMirror || isDownloading}
disabled={!selectedMirror || isDownloading || selectedMirror === 'manual'}
className="download-button"
>
{isDownloading ? <FaSpinner className="fa-spin" /> : <FaDownload />} Download

View File

@ -1,7 +1,7 @@
import React, { useState, useEffect } from "react";
import { FaFolder, FaFile, FaChevronLeft, FaSync, FaRocket, FaSpinner, FaCheck } from "react-icons/fa";
import useAppsStore from "../store";
import { DownloadItem, PackageManifest } from "../types/Apps";
import { DownloadItem, PackageManifest, PackageState } from "../types/Apps";
export default function MyDownloadsPage() {
const { fetchDownloads, fetchDownloadsForApp, startMirroring, stopMirroring, installApp, getCaps, approveCaps, fetchInstalled, installed } = useAppsStore();
@ -19,13 +19,18 @@ export default function MyDownloadsPage() {
}, [currentPath]);
const loadItems = async () => {
let downloads: DownloadItem[];
if (currentPath.length === 0) {
downloads = await fetchDownloads();
} else {
downloads = await fetchDownloadsForApp(currentPath.join(':'));
try {
let downloads: DownloadItem[];
if (currentPath.length === 0) {
downloads = await fetchDownloads();
} else {
downloads = await fetchDownloadsForApp(currentPath.join(':'));
}
setItems(downloads);
} catch (error) {
console.error("Error loading items:", error);
setError(`Error loading items: ${error instanceof Error ? error.message : String(error)}`);
}
setItems(downloads);
};
const navigateToItem = (item: DownloadItem) => {
@ -89,6 +94,11 @@ export default function MyDownloadsPage() {
}
};
const isAppInstalled = (name: string): boolean => {
const packageName = name.replace('.zip', '');
return Object.values(installed).some(app => app.package_id.package_name === packageName);
};
return (
<div className="downloads-page">
<h2>Downloads</h2>
@ -115,7 +125,7 @@ export default function MyDownloadsPage() {
{items.map((item, index) => {
const isFile = !!item.File;
const name = isFile ? item.File!.name : item.Dir!.name;
const isInstalled = isFile && installed.some(i => i.package_id.package_name === name.replace('.zip', ''));
const isInstalled = isFile && isAppInstalled(name);
return (
<tr key={index} onClick={() => navigateToItem(item)} className={isFile ? 'file' : 'directory'}>
<td>

View File

@ -8,7 +8,6 @@ import { kinohash } from '../utils/kinohash';
import useAppsStore from "../store";
export default function PublishPage() {
const { state } = useLocation();
const { openConnectModal } = useConnectModal();
const { ourApps, fetchOurApps } = useAppsStore();
const publicClient = usePublicClient();
@ -83,7 +82,6 @@ export default function PublishPage() {
[tba, owner, _data] = data as [string, string, string];
isUpdate = false; // It's a new package, but we might have a publisher TBA
currentTBA = (tba && tba !== '0x') ? tba as `0x${string}` : null;
console.log('NEWcurrenttba, isupdate: ', currentTBA, isUpdate)
}
let metadata = metadataHash;
@ -252,10 +250,10 @@ export default function PublishPage() {
<div className="my-packages">
<h2>Packages You Own</h2>
{ourApps.length > 0 ? (
{Object.keys(ourApps).length > 0 ? (
<ul>
{ourApps.map((app) => (
<li key={`${app.package_id.package_name}:{app.package_id.publisher_node}`}>
{Object.values(ourApps).map((app) => (
<li key={`${app.package_id.package_name}:${app.package_id.publisher_node}`}>
<Link to={`/app/${app.package_id.package_name}:${app.package_id.publisher_node}`} className="app-name">
{app.metadata?.name || app.package_id.package_name}
</Link>

View File

@ -11,12 +11,10 @@ export default function StorePage() {
fetchListings();
}, [fetchListings]);
const filteredApps = Array.isArray(listings)
? listings.filter((app) =>
app.package_id.package_name.toLowerCase().includes(searchQuery.toLowerCase()) ||
app.metadata?.description?.toLowerCase().includes(searchQuery.toLowerCase())
)
: [];
const filteredApps = Object.values(listings).filter((app) =>
app.package_id.package_name.toLowerCase().includes(searchQuery.toLowerCase()) ||
app.metadata?.description?.toLowerCase().includes(searchQuery.toLowerCase())
);
return (
<div className="store-page">
@ -29,9 +27,9 @@ export default function StorePage() {
/>
</div>
<div className="app-list">
{!Array.isArray(listings) ? (
{Object.keys(listings).length === 0 ? (
<p>Loading...</p>
) : listings.length === 0 ? (
) : filteredApps.length === 0 ? (
<p>No apps available.</p>
) : (
<table>
@ -55,7 +53,6 @@ export default function StorePage() {
);
}
// ... rest of the code remains the same
interface AppRowProps {
app: AppListing;
}

View File

@ -1,70 +0,0 @@
import React, { useState, useEffect } from 'react'
import useAppsStore from '../store'
const Testing: React.FC = () => {
const {
fetchListings,
fetchInstalled,
fetchDownloads,
fetchOurApps,
fetchDownloadsForApp,
listings,
installed,
downloads,
ourApps
} = useAppsStore()
const [result, setResult] = useState<any>(null)
const [appId, setAppId] = useState('')
useEffect(() => {
fetchListings()
fetchInstalled()
fetchDownloads()
fetchOurApps()
}, [])
const handleAction = async (action: () => Promise<void>, key: string) => {
console.log('in handleAction')
try {
await action()
setResult(JSON.stringify(useAppsStore.getState()[key], null, 2))
} catch (error) {
setResult(`Error: ${error.message}`)
}
}
const handleDownloadsForApp = async () => {
try {
const data = await fetchDownloadsForApp(appId)
setResult(JSON.stringify(data, null, 2))
} catch (error) {
setResult(`Error: ${error.message}`)
}
}
return (
<div>
<h1>Testing Page</h1>
<div>
<button onClick={() => handleAction(fetchListings, 'listings')}>Refresh Listings</button>
<button onClick={() => handleAction(fetchInstalled, 'installed')}>Refresh Installed Apps</button>
{/* <button onClick={() => handleAction(fetchDownloads, 'downloads')}>Refresh Downloads</button> */}
<button onClick={() => handleAction(fetchOurApps, 'ourApps')}>Refresh Our Apps</button>
</div>
<div>
<input
type="text"
value={appId}
onChange={(e) => setAppId(e.target.value)}
placeholder="Enter App ID"
/>
<button onClick={handleDownloadsForApp}>
Get Downloads for App
</button>
</div>
<pre>{result}</pre>
</div>
)
}
export default Testing

View File

@ -8,8 +8,8 @@ import { WEBSOCKET_URL } from '../utils/ws'
const BASE_URL = '/main:app_store:sys'
interface AppsStore {
listings: AppListing[]
installed: PackageState[]
listings: Record<string, AppListing>
installed: Record<string, PackageState>
downloads: Record<string, DownloadItem[]>
ourApps: AppListing[]
ws: KinodeClientApi
@ -18,9 +18,10 @@ interface AppsStore {
fetchListings: () => Promise<void>
fetchListing: (id: string) => Promise<AppListing>
fetchInstalled: () => Promise<void>
fetchDownloads: () => Promise<DownloadItem[]>;
fetchOurApps: () => Promise<void>;
fetchDownloadsForApp: (id: string) => Promise<DownloadItem[]>;
fetchInstalledApp: (id: string) => Promise<PackageState | null>
fetchDownloads: () => Promise<DownloadItem[]>
fetchOurApps: () => Promise<void>
fetchDownloadsForApp: (id: string) => Promise<DownloadItem[]>
checkMirror: (node: string) => Promise<MirrorCheckFile>
installApp: (id: string, version_hash: string) => Promise<void>
@ -42,8 +43,8 @@ const appId = (id: string): PackageId => {
const useAppsStore = create<AppsStore>()(
persist(
(set, get): AppsStore => ({
listings: [],
installed: [],
listings: {},
installed: {},
downloads: {},
ourApps: [],
activeDownloads: {},
@ -77,31 +78,51 @@ const useAppsStore = create<AppsStore>()(
fetchListings: async () => {
const res = await fetch(`${BASE_URL}/apps`)
if (res.status === HTTP_STATUS.OK) {
const data = await res.json()
set({ listings: data || [] })
const data: AppListing[] = await res.json()
const listingsMap = data.reduce((acc, listing) => {
acc[`${listing.package_id.package_name}:${listing.package_id.publisher_node}`] = listing
return acc
}, {} as Record<string, AppListing>)
set({ listings: listingsMap })
}
},
fetchListing: async (id: string) => {
const res = await fetch(`${BASE_URL}/apps/${id}`)
if (res.status === HTTP_STATUS.OK) {
const listing = await res.json()
const listing: AppListing = await res.json()
set((state) => ({
listings: state.listings.map(l => l.package_id === appId(id) ? listing : l)
listings: { ...state.listings, [id]: listing }
}))
return listing
}
throw new Error(`Failed to get listing for app: ${id}`)
throw new Error(`Failed to fetch listing for app: ${id}`)
},
fetchInstalled: async () => {
const res = await fetch(`${BASE_URL}/installed`)
if (res.status === HTTP_STATUS.OK) {
const installed = await res.json()
set({ installed })
const data: PackageState[] = await res.json()
const installedMap = data.reduce((acc, pkg) => {
acc[`${pkg.package_id.package_name}:${pkg.package_id.publisher_node}`] = pkg
return acc
}, {} as Record<string, PackageState>)
set({ installed: installedMap })
}
},
fetchInstalledApp: async (id: string) => {
const res = await fetch(`${BASE_URL}/installed/${id}`)
if (res.status === HTTP_STATUS.OK) {
const installedApp: PackageState = await res.json()
set((state) => ({
installed: { ...state.installed, [id]: installedApp }
}))
return installedApp
}
return null
},
fetchDownloads: async () => {
const res = await fetch(`${BASE_URL}/downloads`)
if (res.status === HTTP_STATUS.OK) {
@ -114,17 +135,16 @@ const useAppsStore = create<AppsStore>()(
fetchOurApps: async () => {
const res = await fetch(`${BASE_URL}/ourapps`)
if (res.status === HTTP_STATUS.OK) {
const data = await res.json()
set({ ourApps: data || [] })
const data: AppListing[] = await res.json()
set({ ourApps: data })
}
},
fetchDownloadsForApp: async (id: string) => {
const res = await fetch(`${BASE_URL}/downloads/${id}`)
if (res.status === HTTP_STATUS.OK) {
const downloads = await res.json()
const downloads: DownloadItem[] = await res.json()
set((state) => ({
downloads: { ...state.downloads, [id]: downloads }
}))
@ -136,7 +156,7 @@ const useAppsStore = create<AppsStore>()(
checkMirror: async (node: string) => {
const res = await fetch(`${BASE_URL}/mirrorcheck/${node}`)
if (res.status === HTTP_STATUS.OK) {
return await res.json()
return await res.json() as MirrorCheckFile
}
throw new Error(`Failed to check mirror status for node: ${node}`)
},
@ -173,7 +193,7 @@ const useAppsStore = create<AppsStore>()(
getCaps: async (id: string) => {
const res = await fetch(`${BASE_URL}/apps/${id}/caps`)
if (res.status === HTTP_STATUS.OK) {
return await res.json()
return await res.json() as PackageManifest
}
throw new Error(`Failed to get caps for app: ${id}`)
},

View File

@ -0,0 +1,12 @@
// Helper function to compare version strings
export const compareVersions = (v1: string, v2: string) => {
const parts1 = v1.split('.').map(Number);
const parts2 = v2.split('.').map(Number);
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
const part1 = parts1[i] || 0;
const part2 = parts2[i] || 0;
if (part1 > part2) return 1;
if (part1 < part2) return -1;
}
return 0;
};