mirror of
https://github.com/uqbar-dao/nectar.git
synced 2024-11-25 07:07:06 +03:00
app_store: fix zip and start, downloads display
This commit is contained in:
parent
b666d7a9f9
commit
59cfbaa59b
12
Cargo.lock
generated
12
Cargo.lock
generated
@ -1294,9 +1294,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.1.11"
|
||||
version = "1.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5fb8dd288a69fc53a1996d7ecfbf4a20d59065bff137ce7e56bbd620de191189"
|
||||
checksum = "68064e60dbf1f17005c2fde4d07c16d8baa506fd7ffed8ccab702d93617975c7"
|
||||
dependencies = [
|
||||
"jobserver",
|
||||
"libc",
|
||||
@ -1462,9 +1462,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cmake"
|
||||
version = "0.1.50"
|
||||
version = "0.1.51"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130"
|
||||
checksum = "fb1e43aa7fd152b1f968787f7dbcdeb306d1867ff373c69955211876c053f91a"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
@ -5138,9 +5138,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.124"
|
||||
version = "1.0.125"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "66ad62847a56b3dba58cc891acd13884b9c61138d330c0d7b6181713d4fce38d"
|
||||
checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
|
@ -566,19 +566,49 @@ fn serve_paths(
|
||||
));
|
||||
};
|
||||
|
||||
// TODO move to downloads.
|
||||
let downloads = Address::from_str("downloads@downloads:app_store:sys")?;
|
||||
|
||||
match method {
|
||||
// start mirroring an app
|
||||
// Method::PUT => {
|
||||
// state.start_mirroring(&package_id);
|
||||
// Ok((StatusCode::OK, None, vec![]))
|
||||
// }
|
||||
// // stop mirroring an app
|
||||
// Method::DELETE => {
|
||||
// state.stop_mirroring(&package_id);
|
||||
// Ok((StatusCode::OK, None, vec![]))
|
||||
// }
|
||||
Method::PUT => {
|
||||
let resp = Request::new()
|
||||
.target(downloads)
|
||||
.body(serde_json::to_vec(&DownloadRequests::StartMirroring(
|
||||
crate::kinode::process::main::PackageId::from_process_lib(package_id),
|
||||
))?)
|
||||
.send_and_await_response(5)??;
|
||||
let msg = serde_json::from_slice::<DownloadResponses>(resp.body())?;
|
||||
match msg {
|
||||
DownloadResponses::Success => Ok((StatusCode::OK, None, vec![])),
|
||||
DownloadResponses::Error(e) => {
|
||||
Err(anyhow::anyhow!("Error starting mirroring: {:?}", e))
|
||||
}
|
||||
_ => Err(anyhow::anyhow!(
|
||||
"Invalid response from downloads: {:?}",
|
||||
msg
|
||||
)),
|
||||
}
|
||||
}
|
||||
// stop mirroring an app
|
||||
Method::DELETE => {
|
||||
let resp = Request::new()
|
||||
.target(downloads)
|
||||
.body(serde_json::to_vec(&DownloadRequests::StopMirroring(
|
||||
crate::kinode::process::main::PackageId::from_process_lib(package_id),
|
||||
))?)
|
||||
.send_and_await_response(5)??;
|
||||
let msg = serde_json::from_slice::<DownloadResponses>(resp.body())?;
|
||||
match msg {
|
||||
DownloadResponses::Success => Ok((StatusCode::OK, None, vec![])),
|
||||
DownloadResponses::Error(e) => {
|
||||
Err(anyhow::anyhow!("Error stopping mirroring: {:?}", e))
|
||||
}
|
||||
_ => Err(anyhow::anyhow!(
|
||||
"Invalid response from downloads: {:?}",
|
||||
msg
|
||||
)),
|
||||
}
|
||||
}
|
||||
_ => Ok((
|
||||
StatusCode::METHOD_NOT_ALLOWED,
|
||||
None,
|
||||
|
@ -17,7 +17,7 @@ sha3 = "0.10.8"
|
||||
url = "2.4.1"
|
||||
urlencoding = "2.1.0"
|
||||
wit-bindgen = "0.24.0"
|
||||
zip = { version = "1.1.4", default-features = false }
|
||||
zip = { version = "1.1.4", default-features = false, features = ["deflate"] }
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
@ -3,8 +3,8 @@
|
||||
//! manages downloading and sharing of versioned packages.
|
||||
//!
|
||||
use crate::kinode::process::downloads::{
|
||||
DirEntry, DownloadError, DownloadRequests, DownloadResponses, Entry, FileEntry,
|
||||
LocalDownloadRequest, ProgressUpdate, RemoteDownloadRequest,
|
||||
DirEntry, DownloadRequests, DownloadResponses, Entry, FileEntry, LocalDownloadRequest,
|
||||
ProgressUpdate, RemoteDownloadRequest,
|
||||
};
|
||||
use std::{collections::HashSet, io::Read, str::FromStr};
|
||||
|
||||
@ -15,7 +15,7 @@ use kinode_process_lib::{
|
||||
self,
|
||||
client::{HttpClientError, HttpClientResponse},
|
||||
},
|
||||
print_to_terminal, println,
|
||||
print_to_terminal, println, set_state,
|
||||
vfs::{self, Directory, File},
|
||||
Address, Message, PackageId, ProcessId, Request, Response,
|
||||
};
|
||||
@ -214,9 +214,9 @@ fn handle_message(
|
||||
}
|
||||
};
|
||||
|
||||
Response::new()
|
||||
.body(serde_json::to_string(&files)?)
|
||||
.send()?;
|
||||
let resp = DownloadResponses::GetFiles(files);
|
||||
|
||||
Response::new().body(serde_json::to_string(&resp)?).send()?;
|
||||
}
|
||||
DownloadRequests::AddDownload(add_req) => {
|
||||
if !message.is_local(our) {
|
||||
@ -230,7 +230,7 @@ fn handle_message(
|
||||
let package_dir = format!(
|
||||
"{}/{}",
|
||||
downloads.path,
|
||||
add_req.package_id.to_process_lib().to_string()
|
||||
add_req.package_id.clone().to_process_lib().to_string()
|
||||
);
|
||||
let _ = vfs::open_dir(&package_dir, true, None);
|
||||
|
||||
@ -244,6 +244,12 @@ fn handle_message(
|
||||
let manifest_path = format!("{}/{}.json", package_dir, add_req.version_hash);
|
||||
extract_and_write_manifest(&bytes, &manifest_path)?;
|
||||
|
||||
// add mirrors if applicable and save:
|
||||
if add_req.mirror {
|
||||
state.mirroring.insert(add_req.package_id.to_process_lib());
|
||||
set_state(&serde_json::to_vec(&state)?);
|
||||
}
|
||||
|
||||
Response::new()
|
||||
.body(serde_json::to_vec(&Resp::Download(
|
||||
DownloadResponses::Success,
|
||||
@ -253,6 +259,7 @@ fn handle_message(
|
||||
DownloadRequests::StartMirroring(package_id) => {
|
||||
let package_id = package_id.to_process_lib();
|
||||
state.mirroring.insert(package_id);
|
||||
set_state(&serde_json::to_vec(&state)?);
|
||||
Response::new()
|
||||
.body(serde_json::to_vec(&Resp::Download(
|
||||
DownloadResponses::Success,
|
||||
@ -262,6 +269,7 @@ fn handle_message(
|
||||
DownloadRequests::StopMirroring(package_id) => {
|
||||
let package_id = package_id.to_process_lib();
|
||||
state.mirroring.remove(&package_id);
|
||||
set_state(&serde_json::to_vec(&state)?);
|
||||
Response::new()
|
||||
.body(serde_json::to_vec(&Resp::Download(
|
||||
DownloadResponses::Success,
|
||||
@ -337,41 +345,31 @@ fn format_entries(entries: Vec<vfs::DirEntry>, state: &State) -> Vec<Entry> {
|
||||
entries
|
||||
.into_iter()
|
||||
.filter_map(|entry| {
|
||||
let name = entry
|
||||
.path
|
||||
.rsplit('/')
|
||||
.next()
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
let name = entry.path.split('/').last()?.to_string();
|
||||
let is_file = entry.file_type == vfs::FileType::File;
|
||||
|
||||
if is_file {
|
||||
if name.ends_with(".zip") {
|
||||
let size = vfs::metadata(&entry.path, None)
|
||||
.ok()
|
||||
.map(|meta| meta.len)
|
||||
.unwrap_or(0);
|
||||
let json_path = entry.path.replace(".zip", ".json");
|
||||
let manifest = if let Ok(file) = vfs::open_file(&json_path, false, None) {
|
||||
file.read_to_string().ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Some(Entry::File(FileEntry {
|
||||
name,
|
||||
size,
|
||||
manifest: manifest.unwrap_or_default(),
|
||||
}))
|
||||
} else {
|
||||
None // ignore non-zip files
|
||||
}
|
||||
} else {
|
||||
let mirroring = PackageId::from_str(&name)
|
||||
.map(|package_id| state.mirroring.contains(&package_id))
|
||||
.unwrap_or(false);
|
||||
if is_file && name.ends_with(".zip") {
|
||||
let size = vfs::metadata(&entry.path, None)
|
||||
.map(|meta| meta.len)
|
||||
.unwrap_or(0);
|
||||
let json_path = entry.path.replace(".zip", ".json");
|
||||
let manifest = vfs::open_file(&json_path, false, None)
|
||||
.and_then(|file| file.read_to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
Some(Entry::File(FileEntry {
|
||||
name,
|
||||
size,
|
||||
manifest,
|
||||
}))
|
||||
} else if !is_file {
|
||||
let mirroring = state.mirroring.iter().any(|pid| {
|
||||
pid.package_name == name
|
||||
|| format!("{}:{}", pid.package_name, pid.publisher_node) == name
|
||||
});
|
||||
Some(Entry::Dir(DirEntry { name, mirroring }))
|
||||
} else {
|
||||
None // Skip non-zip files
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
@ -398,14 +396,6 @@ fn extract_and_write_manifest(file_contents: &[u8], manifest_path: &str) -> anyh
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// note this, might be tricky:
|
||||
// when ready, extract + write the damn manifest to a file location baby!
|
||||
|
||||
// let wit_version = match metadata {
|
||||
// Some(metadata) => metadata.properties.wit_version,
|
||||
// None => Some(0),
|
||||
// };
|
||||
|
||||
/// helper function for vfs files, open if exists, if not create
|
||||
fn open_or_create_file(path: &str) -> anyhow::Result<File> {
|
||||
match vfs::open_file(path, false, None) {
|
||||
|
@ -15,7 +15,7 @@ serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
sha2 = "0.10.8"
|
||||
wit-bindgen = "0.24.0"
|
||||
zip = { version = "1.1.4", default-features = false }
|
||||
zip = { version = "1.1.4", default-features = false, features = ["deflate"] }
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
@ -2,7 +2,7 @@ import { parseAbi } from "viem";
|
||||
|
||||
export { encodeMulticalls, encodeIntoMintCall } from "./helpers";
|
||||
|
||||
export const KIMAP: `0x${string}` = "0xAfA2e57D3cBA08169b416457C14eBA2D6021c4b5";
|
||||
export const KIMAP: `0x${string}` = "0xcA92476B2483aBD5D82AEBF0b56701Bb2e9be658";
|
||||
export const MULTICALL: `0x${string}` = "0xcA11bde05977b3631167028862bE2a173976CA11";
|
||||
export const KINO_ACCOUNT_IMPL: `0x${string}` = "0x38766C70a4FB2f23137D9251a1aA12b1143fC716";
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useEffect, useState, useCallback } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { FaDownload, FaCheck, FaTimes, FaPlay } from "react-icons/fa";
|
||||
import useAppsStore from "../store";
|
||||
@ -13,37 +13,37 @@ export default function AppPage() {
|
||||
const [currentVersion, setCurrentVersion] = useState<string | null>(null);
|
||||
const [latestVersion, setLatestVersion] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const loadData = async () => {
|
||||
await Promise.all([fetchListings(), fetchInstalled()]);
|
||||
const loadData = useCallback(async () => {
|
||||
await Promise.all([fetchListings(), fetchInstalled()]);
|
||||
|
||||
const foundApp = listings.find(a => `${a.package_id.package_name}:${a.package_id.publisher_node}` === id) || null;
|
||||
setApp(foundApp);
|
||||
const foundApp = listings.find(a => `${a.package_id.package_name}:${a.package_id.publisher_node}` === id) || null;
|
||||
setApp(foundApp);
|
||||
|
||||
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);
|
||||
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);
|
||||
|
||||
if (foundApp.metadata?.properties?.code_hashes) {
|
||||
const versions = foundApp.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);
|
||||
if (installedVersion) {
|
||||
setCurrentVersion(installedVersion[0]);
|
||||
}
|
||||
if (foundApp.metadata?.properties?.code_hashes) {
|
||||
const versions = foundApp.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);
|
||||
if (installedVersion) {
|
||||
setCurrentVersion(installedVersion[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}, [id, fetchListings, fetchInstalled]);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, [id, fetchListings, fetchInstalled, listings, installed]);
|
||||
}, [loadData]);
|
||||
|
||||
if (!app) {
|
||||
return <div className="app-page"><h4>App details not found for {id}</h4></div>;
|
||||
|
@ -6,7 +6,7 @@ import { AppListing, DownloadItem, MirrorCheckFile, PackageManifest } from "../t
|
||||
|
||||
export default function DownloadPage() {
|
||||
const { id } = useParams();
|
||||
const { listings, fetchListings, fetchDownloadsForApp, downloadApp, installApp, checkMirror, fetchInstalled, installed, getCaps } = useAppsStore();
|
||||
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 }>({});
|
||||
@ -17,6 +17,8 @@ export default function DownloadPage() {
|
||||
const [showManifest, setShowManifest] = useState(false);
|
||||
const [showCaps, setShowCaps] = useState(false);
|
||||
const [manifest, setManifest] = useState<PackageManifest | null>(null);
|
||||
const [showCapApproval, setShowCapApproval] = useState(false);
|
||||
const [selectedVersion, setSelectedVersion] = useState<string | null>(null);
|
||||
|
||||
const app = listings.find(a => `${a.package_id.package_name}:${a.package_id.publisher_node}` === id);
|
||||
|
||||
@ -64,11 +66,26 @@ export default function DownloadPage() {
|
||||
|
||||
const handleInstall = async (version: string) => {
|
||||
if (!app) return;
|
||||
setSelectedVersion(version);
|
||||
try {
|
||||
const caps = await getCaps(`${app.package_id.package_name}:${app.package_id.publisher_node}`);
|
||||
setManifest(caps);
|
||||
setShowCapApproval(true);
|
||||
} catch (error) {
|
||||
console.error('Failed to get capabilities:', error);
|
||||
setError(`Failed to get capabilities: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
};
|
||||
|
||||
const confirmInstall = async () => {
|
||||
if (!app || !selectedVersion) return;
|
||||
setIsInstalling(true);
|
||||
setError(null);
|
||||
try {
|
||||
await installApp(`${app.package_id.package_name}:${app.package_id.publisher_node}`, version);
|
||||
await approveCaps(`${app.package_id.package_name}:${app.package_id.publisher_node}`);
|
||||
await installApp(`${app.package_id.package_name}:${app.package_id.publisher_node}`, selectedVersion);
|
||||
fetchInstalled();
|
||||
setShowCapApproval(false);
|
||||
} catch (error) {
|
||||
console.error('Installation failed:', error);
|
||||
setError(`Installation failed: ${error instanceof Error ? error.message : String(error)}`);
|
||||
@ -123,14 +140,19 @@ export default function DownloadPage() {
|
||||
</thead>
|
||||
<tbody>
|
||||
{app.metadata?.properties?.code_hashes.map(([version, hash]) => {
|
||||
const download = downloads.find(d => d.name === `${hash}.zip`);
|
||||
const download = downloads.find(d => {
|
||||
if (d.File) {
|
||||
return d.File.name === `${hash}.zip`;
|
||||
}
|
||||
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);
|
||||
|
||||
return (
|
||||
<tr key={version}>
|
||||
<td>{version}</td>
|
||||
<td>{download?.size ? `${(download.size / 1024 / 1024).toFixed(2)} MB` : 'N/A'}</td>
|
||||
<td>{download?.File?.size ? `${(download.File.size / 1024 / 1024).toFixed(2)} MB` : 'N/A'}</td>
|
||||
<td>
|
||||
{isInstalled ? 'Installed' : isDownloaded ? 'Downloaded' : 'Not downloaded'}
|
||||
</td>
|
||||
@ -207,6 +229,21 @@ export default function DownloadPage() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showCapApproval && manifest && (
|
||||
<div className="cap-approval-popup">
|
||||
<h3>Approve Capabilities</h3>
|
||||
<pre className="json-display">
|
||||
{JSON.stringify(manifest.request_capabilities, null, 2)}
|
||||
</pre>
|
||||
<div className="approval-buttons">
|
||||
<button onClick={() => setShowCapApproval(false)}>Cancel</button>
|
||||
<button onClick={confirmInstall} disabled={isInstalling}>
|
||||
{isInstalling ? <FaSpinner className="fa-spin" /> : 'Approve and Install'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,15 +1,21 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { FaFolder, FaFile, FaChevronLeft } from "react-icons/fa";
|
||||
import { FaFolder, FaFile, FaChevronLeft, FaSync, FaRocket, FaSpinner, FaCheck } from "react-icons/fa";
|
||||
import useAppsStore from "../store";
|
||||
import { DownloadItem } from "../types/Apps";
|
||||
import { DownloadItem, PackageManifest } from "../types/Apps";
|
||||
|
||||
export default function MyDownloadsPage() {
|
||||
const { fetchDownloads, fetchDownloadsForApp, listings } = useAppsStore();
|
||||
const { fetchDownloads, fetchDownloadsForApp, startMirroring, stopMirroring, installApp, getCaps, approveCaps, fetchInstalled, installed } = useAppsStore();
|
||||
const [currentPath, setCurrentPath] = useState<string[]>([]);
|
||||
const [items, setItems] = useState<DownloadItem[]>([]);
|
||||
const [isInstalling, setIsInstalling] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [showCapApproval, setShowCapApproval] = useState(false);
|
||||
const [manifest, setManifest] = useState<PackageManifest | null>(null);
|
||||
const [selectedItem, setSelectedItem] = useState<DownloadItem | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadItems();
|
||||
fetchInstalled();
|
||||
}, [currentPath]);
|
||||
|
||||
const loadItems = async () => {
|
||||
@ -23,8 +29,8 @@ export default function MyDownloadsPage() {
|
||||
};
|
||||
|
||||
const navigateToItem = (item: DownloadItem) => {
|
||||
if (!item.is_file) {
|
||||
setCurrentPath([...currentPath, item.name]);
|
||||
if (item.Dir) {
|
||||
setCurrentPath([...currentPath, item.Dir.name]);
|
||||
}
|
||||
};
|
||||
|
||||
@ -32,20 +38,55 @@ export default function MyDownloadsPage() {
|
||||
setCurrentPath(currentPath.slice(0, -1));
|
||||
};
|
||||
|
||||
const getItemVersion = (name: string): string | undefined => {
|
||||
if (currentPath.length === 0) {
|
||||
return undefined; // No version for top-level directories
|
||||
const toggleMirroring = async (item: DownloadItem) => {
|
||||
if (item.Dir) {
|
||||
const packageId = [...currentPath, item.Dir.name].join(':');
|
||||
try {
|
||||
if (item.Dir.mirroring) {
|
||||
await stopMirroring(packageId);
|
||||
} else {
|
||||
await startMirroring(packageId);
|
||||
}
|
||||
await loadItems();
|
||||
} catch (error) {
|
||||
console.error("Error toggling mirroring:", error);
|
||||
setError(`Error toggling mirroring: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const appId = currentPath.join(':');
|
||||
const app = listings.find(a => `${a.package_id.package_name}:${a.package_id.publisher_node}` === appId);
|
||||
|
||||
if (app && app.metadata?.properties?.code_hashes) {
|
||||
const matchingHash = app.metadata.properties.code_hashes.find(([_, hash]) => `${hash}.zip` === name);
|
||||
return matchingHash ? matchingHash[0] : undefined;
|
||||
const handleInstall = async (item: DownloadItem) => {
|
||||
if (item.File) {
|
||||
setSelectedItem(item);
|
||||
try {
|
||||
const packageId = [...currentPath, item.File.name.replace('.zip', '')].join(':');
|
||||
const caps = await getCaps(packageId);
|
||||
setManifest(caps);
|
||||
setShowCapApproval(true);
|
||||
} catch (error) {
|
||||
console.error('Failed to get capabilities:', error);
|
||||
setError(`Failed to get capabilities: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return undefined;
|
||||
const confirmInstall = async () => {
|
||||
if (!selectedItem?.File) return;
|
||||
setIsInstalling(true);
|
||||
setError(null);
|
||||
try {
|
||||
const packageId = [...currentPath, selectedItem.File.name.replace('.zip', '')].join(':');
|
||||
await approveCaps(packageId);
|
||||
await installApp(packageId, selectedItem.File.manifest);
|
||||
await fetchInstalled();
|
||||
setShowCapApproval(false);
|
||||
await loadItems();
|
||||
} catch (error) {
|
||||
console.error('Installation failed:', error);
|
||||
setError(`Installation failed: ${error instanceof Error ? error.message : String(error)}`);
|
||||
} finally {
|
||||
setIsInstalling(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@ -66,26 +107,65 @@ export default function MyDownloadsPage() {
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Size</th>
|
||||
<th>Version</th>
|
||||
<th>Mirroring</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.map((item, index) => {
|
||||
const version = getItemVersion(item.name);
|
||||
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', ''));
|
||||
return (
|
||||
<tr key={index} onClick={() => navigateToItem(item)} className={item.is_file ? 'file' : 'directory'}>
|
||||
<tr key={index} onClick={() => navigateToItem(item)} className={isFile ? 'file' : 'directory'}>
|
||||
<td>
|
||||
{item.is_file ? <FaFile /> : <FaFolder />} {item.name}
|
||||
{isFile ? <FaFile /> : <FaFolder />} {name}
|
||||
</td>
|
||||
<td>{isFile ? 'File' : 'Directory'}</td>
|
||||
<td>{isFile ? `${(item.File!.size / 1024).toFixed(2)} KB` : '-'}</td>
|
||||
<td>{!isFile && (item.Dir!.mirroring ? 'Yes' : 'No')}</td>
|
||||
<td>
|
||||
{!isFile && (
|
||||
<button onClick={(e) => { e.stopPropagation(); toggleMirroring(item); }}>
|
||||
<FaSync /> {item.Dir!.mirroring ? 'Stop' : 'Start'} Mirroring
|
||||
</button>
|
||||
)}
|
||||
{isFile && !isInstalled && (
|
||||
<button onClick={(e) => { e.stopPropagation(); handleInstall(item); }}>
|
||||
<FaRocket /> Install
|
||||
</button>
|
||||
)}
|
||||
{isFile && isInstalled && (
|
||||
<FaCheck className="installed" />
|
||||
)}
|
||||
</td>
|
||||
<td>{item.is_file ? 'File' : 'Directory'}</td>
|
||||
<td>{item.size ? `${(item.size / 1024).toFixed(2)} KB` : '-'}</td>
|
||||
<td>{version || '-'}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="error-message">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showCapApproval && manifest && (
|
||||
<div className="cap-approval-popup">
|
||||
<h3>Approve Capabilities</h3>
|
||||
<pre className="json-display">
|
||||
{JSON.stringify(manifest.request_capabilities, null, 2)}
|
||||
</pre>
|
||||
<div className="approval-buttons">
|
||||
<button onClick={() => setShowCapApproval(false)}>Cancel</button>
|
||||
<button onClick={confirmInstall} disabled={isInstalling}>
|
||||
{isInstalling ? <FaSpinner className="fa-spin" /> : 'Approve and Install'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -59,6 +59,7 @@ export default function PublishPage() {
|
||||
|
||||
try {
|
||||
// Check if the package already exists and get its TBA
|
||||
console.log('packageName, publisherId: ', packageName, publisherId)
|
||||
let data = await publicClient.readContract({
|
||||
abi: kimapAbi,
|
||||
address: KIMAP,
|
||||
@ -69,7 +70,7 @@ export default function PublishPage() {
|
||||
let [tba, owner, _data] = data as [string, string, string];
|
||||
let isUpdate = Boolean(tba && tba !== '0x' && owner === address);
|
||||
let currentTBA = isUpdate ? tba as `0x${string}` : null;
|
||||
|
||||
console.log('currenttba, isupdate: ', currentTBA, isUpdate)
|
||||
// If the package doesn't exist, check for the publisher's TBA
|
||||
if (!currentTBA) {
|
||||
data = await publicClient.readContract({
|
||||
@ -82,6 +83,7 @@ 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;
|
||||
|
@ -24,6 +24,7 @@ const Testing: React.FC = () => {
|
||||
}, [])
|
||||
|
||||
const handleAction = async (action: () => Promise<void>, key: string) => {
|
||||
console.log('in handleAction')
|
||||
try {
|
||||
await action()
|
||||
setResult(JSON.stringify(useAppsStore.getState()[key], null, 2))
|
||||
|
@ -28,7 +28,8 @@ interface AppsStore {
|
||||
downloadApp: (id: string, version_hash: string, downloadFrom: string) => Promise<void>
|
||||
getCaps: (id: string) => Promise<PackageManifest>
|
||||
approveCaps: (id: string) => Promise<void>
|
||||
setMirroring: (id: string, version_hash: string, mirroring: boolean) => Promise<void>
|
||||
startMirroring: (id: string) => Promise<void>
|
||||
stopMirroring: (id: string) => Promise<void>
|
||||
setAutoUpdate: (id: string, version_hash: string, autoUpdate: boolean) => Promise<void>
|
||||
}
|
||||
|
||||
@ -77,7 +78,7 @@ const useAppsStore = create<AppsStore>()(
|
||||
const res = await fetch(`${BASE_URL}/apps`)
|
||||
if (res.status === HTTP_STATUS.OK) {
|
||||
const data = await res.json()
|
||||
set({ listings: data.apps || [] })
|
||||
set({ listings: data || [] })
|
||||
}
|
||||
},
|
||||
|
||||
@ -104,7 +105,7 @@ const useAppsStore = create<AppsStore>()(
|
||||
fetchDownloads: async () => {
|
||||
const res = await fetch(`${BASE_URL}/downloads`)
|
||||
if (res.status === HTTP_STATUS.OK) {
|
||||
const downloads = await res.json()
|
||||
const downloads: DownloadItem[] = await res.json()
|
||||
set({ downloads: { root: downloads } })
|
||||
return downloads
|
||||
}
|
||||
@ -116,7 +117,7 @@ const useAppsStore = create<AppsStore>()(
|
||||
|
||||
if (res.status === HTTP_STATUS.OK) {
|
||||
const data = await res.json()
|
||||
set({ ourApps: data.apps || [] })
|
||||
set({ ourApps: data || [] })
|
||||
}
|
||||
},
|
||||
|
||||
@ -185,16 +186,24 @@ const useAppsStore = create<AppsStore>()(
|
||||
await get().fetchListing(id)
|
||||
},
|
||||
|
||||
setMirroring: async (id: string, version_hash: string, mirroring: boolean) => {
|
||||
const method = mirroring ? 'PUT' : 'DELETE'
|
||||
const res = await fetch(`${BASE_URL}/apps/${id}/mirror`, {
|
||||
method,
|
||||
body: JSON.stringify({ version_hash })
|
||||
startMirroring: async (id: string) => {
|
||||
const res = await fetch(`${BASE_URL}/downloads/${id}/mirror`, {
|
||||
method: 'PUT'
|
||||
})
|
||||
if (res.status !== HTTP_STATUS.OK) {
|
||||
throw new Error(`Failed to ${mirroring ? 'start' : 'stop'} mirroring: ${id}`)
|
||||
throw new Error(`Failed to start mirroring: ${id}`)
|
||||
}
|
||||
await get().fetchListing(id)
|
||||
await get().fetchDownloadsForApp(id.split(':').slice(0, -1).join(':'))
|
||||
},
|
||||
|
||||
stopMirroring: async (id: string) => {
|
||||
const res = await fetch(`${BASE_URL}/downloads/${id}/mirror`, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
if (res.status !== HTTP_STATUS.OK) {
|
||||
throw new Error(`Failed to stop mirroring: ${id}`)
|
||||
}
|
||||
await get().fetchDownloadsForApp(id.split(':').slice(0, -1).join(':'))
|
||||
},
|
||||
|
||||
setAutoUpdate: async (id: string, version_hash: string, autoUpdate: boolean) => {
|
||||
|
@ -12,10 +12,20 @@ export interface AppListing {
|
||||
auto_update: boolean
|
||||
}
|
||||
|
||||
export interface DownloadItem {
|
||||
name: string,
|
||||
is_file: boolean,
|
||||
size?: number
|
||||
export type DownloadItem = {
|
||||
Dir?: DirItem;
|
||||
File?: FileItem;
|
||||
};
|
||||
|
||||
export interface DirItem {
|
||||
name: string;
|
||||
mirroring: boolean;
|
||||
}
|
||||
|
||||
export interface FileItem {
|
||||
name: string;
|
||||
size: number;
|
||||
manifest: string;
|
||||
}
|
||||
|
||||
export interface MirrorCheckFile {
|
||||
|
Loading…
Reference in New Issue
Block a user