mirror of
https://github.com/uqbar-dao/nectar.git
synced 2024-12-23 16:43:24 +03:00
app_store: new download flow
This commit is contained in:
parent
ca0996aca7
commit
c47e41a87b
@ -24,7 +24,7 @@ use ft_worker_lib::{
|
||||
use kinode_process_lib::{
|
||||
await_message, call_init, eth, get_blob,
|
||||
http::{self, WsMessageType},
|
||||
kimap, vfs, Address, LazyLoadBlob, Message, PackageId, Request, Response,
|
||||
kimap, println, vfs, Address, LazyLoadBlob, Message, PackageId, Request, Response,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use state::{AppStoreLogError, PackageState, RequestedPackage, State};
|
||||
@ -162,7 +162,9 @@ fn handle_message(state: &mut State, message: &Message) -> anyhow::Result<()> {
|
||||
.as_bytes()
|
||||
.to_vec(),
|
||||
};
|
||||
http::send_ws_push(6969, WsMessageType::Text, ws_blob);
|
||||
for channel_id in state.ui_ws_channels.iter() {
|
||||
http::send_ws_push(*channel_id, WsMessageType::Text, ws_blob.clone());
|
||||
}
|
||||
}
|
||||
Req::FTWorkerResult(r) => {
|
||||
println!("got weird ft_worker result: {r:?}");
|
||||
@ -192,6 +194,10 @@ fn handle_message(state: &mut State, message: &Message) -> anyhow::Result<()> {
|
||||
}
|
||||
if let http::HttpServerRequest::Http(req) = incoming {
|
||||
http_api::handle_http_request(state, &req)?;
|
||||
} else if let http::HttpServerRequest::WebSocketOpen { channel_id, .. } = incoming {
|
||||
state.ui_ws_channels.insert(channel_id);
|
||||
} else if let http::HttpServerRequest::WebSocketClose { 0: channel_id } = incoming {
|
||||
state.ui_ws_channels.remove(&channel_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -117,6 +117,8 @@ pub struct State {
|
||||
pub requested_packages: HashMap<PackageId, RequestedPackage>,
|
||||
/// the APIs we have outstanding requests to download (not persisted)
|
||||
pub requested_apis: HashMap<PackageId, RequestedPackage>,
|
||||
/// UI websocket connected channel_IDs
|
||||
pub ui_ws_channels: HashSet<u32>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@ -152,6 +154,7 @@ impl State {
|
||||
downloaded_apis: s.downloaded_apis,
|
||||
requested_packages: HashMap::new(),
|
||||
requested_apis: HashMap::new(),
|
||||
ui_ws_channels: HashSet::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -166,6 +169,7 @@ impl State {
|
||||
downloaded_apis: HashSet::new(),
|
||||
requested_packages: HashMap::new(),
|
||||
requested_apis: HashMap::new(),
|
||||
ui_ws_channels: HashSet::new(),
|
||||
};
|
||||
state.populate_packages_from_filesystem()?;
|
||||
Ok(state)
|
||||
@ -210,8 +214,10 @@ impl State {
|
||||
mirroring: package_state.mirroring,
|
||||
auto_update: package_state.auto_update,
|
||||
})?)?;
|
||||
if utils::extract_api(package_id)? {
|
||||
self.downloaded_apis.insert(package_id.to_owned());
|
||||
if let Ok(extracted) = utils::extract_api(package_id) {
|
||||
if extracted {
|
||||
self.downloaded_apis.insert(package_id.to_owned());
|
||||
}
|
||||
}
|
||||
listing.state = Some(package_state);
|
||||
// kinode_process_lib::set_state(&serde_json::to_vec(self)?);
|
||||
|
@ -64,7 +64,7 @@ fn handle_send(our: &Address, target: &Address, file_name: &str, timeout: u64) -
|
||||
let file_bytes = blob.bytes;
|
||||
let mut file_size = file_bytes.len() as u64;
|
||||
let mut offset: u64 = 0;
|
||||
let chunk_size: u64 = 1048576; // 1MB, can be changed
|
||||
let chunk_size: u64 = 262144; // 256KB
|
||||
let total_chunks = (file_size as f64 / chunk_size as f64).ceil() as u64;
|
||||
// send a file to another worker
|
||||
// start by telling target to expect a file,
|
||||
|
@ -3,9 +3,10 @@ import { BrowserRouter as Router, Route, Routes } from "react-router-dom";
|
||||
|
||||
import StorePage from "./pages/StorePage";
|
||||
import AppPage from "./pages/AppPage";
|
||||
import { APP_DETAILS_PATH, PUBLISH_PATH, STORE_PATH } from "./constants/path";
|
||||
import PublishPage from "./pages/PublishPage";
|
||||
import Header from "./components/Header";
|
||||
import { APP_DETAILS_PATH, PUBLISH_PATH, STORE_PATH } from "./constants/path";
|
||||
|
||||
|
||||
const BASE_URL = import.meta.env.BASE_URL;
|
||||
if (window.our) window.our.process = BASE_URL?.replace("/", "");
|
||||
|
@ -17,6 +17,7 @@
|
||||
font-size: 1.5rem;
|
||||
margin: 0;
|
||||
margin-right: 2rem;
|
||||
color: var(--orange);
|
||||
}
|
||||
|
||||
.header-left nav {
|
||||
@ -44,9 +45,36 @@
|
||||
}
|
||||
|
||||
.app-content {
|
||||
padding: 2rem;
|
||||
flex-grow: 1;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.app-info-column {
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.app-actions-column {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.app-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.app-content {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.app-info-column,
|
||||
.app-actions-column {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.special-appstore-background {
|
||||
@ -58,14 +86,63 @@
|
||||
background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
|
||||
}
|
||||
|
||||
/* Common Styles */
|
||||
button,
|
||||
.external-link {
|
||||
padding: 10px 15px;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-decoration: none;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
button:hover,
|
||||
.external-link:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
button svg,
|
||||
.external-link svg {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.primary {
|
||||
background-color: var(--orange);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.secondary {
|
||||
background-color: var(--gray);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.external-link {
|
||||
background-color: var(--blue);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
/* Store Page Styles */
|
||||
.store-page {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.store-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: stretch;
|
||||
margin-bottom: 2rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
margin-bottom: 1rem;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.search-bar input {
|
||||
@ -74,36 +151,77 @@
|
||||
font-size: 1rem;
|
||||
border: 1px solid var(--gray);
|
||||
border-radius: 4px;
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
.app-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-top: 2rem;
|
||||
.filter-button,
|
||||
.store-header button {
|
||||
height: 38px;
|
||||
padding: 0 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
white-space: nowrap;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.app-card {
|
||||
background-color: light-dark(var(--white), var(--off-black));
|
||||
border: 1px solid var(--gray);
|
||||
border-radius: 8px;
|
||||
.store-header>* {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.store-header button {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.app-list table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.app-list th,
|
||||
.app-list td {
|
||||
padding: 1rem;
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--gray);
|
||||
}
|
||||
|
||||
.app-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.app-card h3 {
|
||||
margin-bottom: 0.5rem;
|
||||
.app-list th {
|
||||
font-weight: bold;
|
||||
color: var(--orange);
|
||||
}
|
||||
|
||||
.app-card p {
|
||||
.app-row:hover {
|
||||
background-color: light-dark(var(--tan), var(--maroon));
|
||||
}
|
||||
|
||||
.app-name {
|
||||
font-weight: bold;
|
||||
color: var(--blue);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.publisher,
|
||||
.version,
|
||||
.mirrors {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.status {
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9rem;
|
||||
color: light-dark(var(--gray), var(--off-white));
|
||||
}
|
||||
|
||||
.status.installed {
|
||||
background-color: var(--off-black);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.status.not-installed {
|
||||
background-color: var(--gray);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
/* App Page Styles */
|
||||
@ -263,45 +381,6 @@
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
button,
|
||||
.external-link {
|
||||
padding: 10px 15px;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-decoration: none;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
button:hover,
|
||||
.external-link:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
button svg,
|
||||
.external-link svg {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.primary {
|
||||
background-color: var(--orange);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.secondary {
|
||||
background-color: var(--gray);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.external-link {
|
||||
background-color: var(--blue);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
/* My Apps Page Styles */
|
||||
.my-apps-page {
|
||||
padding: 2rem;
|
||||
@ -437,7 +516,7 @@ button svg,
|
||||
}
|
||||
|
||||
.message.success {
|
||||
background-color: #4CAF50;
|
||||
background-color: var(--green);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
@ -476,133 +555,7 @@ button svg,
|
||||
background-color: #c62828;
|
||||
}
|
||||
|
||||
/* Store Page Styles */
|
||||
.store-page {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.store-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: stretch;
|
||||
margin-bottom: 2rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.search-bar input {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
font-size: 1rem;
|
||||
border: 1px solid var(--gray);
|
||||
border-radius: 4px;
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
.filter-button,
|
||||
.store-header button {
|
||||
height: 38px;
|
||||
padding: 0 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
white-space: nowrap;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
/* Add these new styles */
|
||||
.store-header>* {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.store-header button {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.app-list table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.app-list th,
|
||||
.app-list td {
|
||||
padding: 1rem;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--gray);
|
||||
}
|
||||
|
||||
.app-list th {
|
||||
font-weight: bold;
|
||||
color: var(--orange);
|
||||
}
|
||||
|
||||
.app-row:hover {
|
||||
background-color: light-dark(var(--tan), var(--maroon));
|
||||
}
|
||||
|
||||
.app-name {
|
||||
font-weight: bold;
|
||||
color: var(--blue);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.publisher,
|
||||
.version,
|
||||
.mirrors {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.status {
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.status.installed {
|
||||
background-color: var(--off-black);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.status.not-installed {
|
||||
background-color: var(--gray);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.app-info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.app-info-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.app-info-item>span:first-child {
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.app-info-item a {
|
||||
color: var(--blue);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.app-info-item a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Mirrors Dropdown Styles */
|
||||
.mirrors-dropdown {
|
||||
position: relative;
|
||||
}
|
||||
@ -613,7 +566,7 @@ button svg,
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--off-black);
|
||||
color: light-dark(var(--off-black), var(--off-white));
|
||||
}
|
||||
|
||||
.mirrors-list {
|
||||
@ -638,8 +591,7 @@ button svg,
|
||||
cursor: pointer;
|
||||
padding: 5px;
|
||||
margin-right: 10px;
|
||||
display: inline-flex !important;
|
||||
/* Force display */
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
@ -649,8 +601,7 @@ button svg,
|
||||
.check-button svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: var(--off-black);
|
||||
/* Ensure visibility */
|
||||
color: light-dark(var(--off-black), var(--off-white));
|
||||
}
|
||||
|
||||
.spinning {
|
||||
@ -673,15 +624,72 @@ button svg,
|
||||
}
|
||||
|
||||
.online {
|
||||
color: green;
|
||||
color: var(--green);
|
||||
}
|
||||
|
||||
.offline {
|
||||
color: red;
|
||||
color: var(--ansi-red);
|
||||
}
|
||||
|
||||
.error-message {
|
||||
margin-left: 5px;
|
||||
font-size: 0.8em;
|
||||
color: #888;
|
||||
color: var(--gray);
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 24px;
|
||||
background-color: var(--gray-light);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.progress {
|
||||
height: 100%;
|
||||
background-color: var(--blue);
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-percentage {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--white);
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
text-shadow: 0 0 2px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.capabilities-section {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.capabilities-section h3 {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
background-color: var(--gray-light);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.capabilities {
|
||||
background-color: var(--off-white);
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
overflow-x: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
@ -1,43 +1,111 @@
|
||||
import React, { useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { FaDownload, FaSync, FaTrash, FaMagnet, FaCog, FaCheck, FaTimes, FaRocket, FaLink, FaChevronDown, FaChevronUp, FaShieldAlt } from "react-icons/fa";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { FaDownload, FaSync, FaTrash, FaMagnet, FaCog, FaCheck, FaTimes, FaRocket, FaLink, FaChevronDown, FaChevronUp, FaShieldAlt, FaSpinner, FaPlay, FaExclamationTriangle } from "react-icons/fa";
|
||||
import useAppsStore from "../store";
|
||||
import { appId } from "../utils/app";
|
||||
import { MirrorCheckFile } from "../types/Apps";
|
||||
|
||||
export default function AppPage() {
|
||||
const { installApp, updateApp, uninstallApp, approveCaps, setMirroring, setAutoUpdate, checkMirror, apps } = useAppsStore();
|
||||
const { installApp, updateApp, uninstallApp, approveCaps, setMirroring, setAutoUpdate, checkMirror, apps, downloadApp, getCaps, getApp } = useAppsStore();
|
||||
const { id } = useParams();
|
||||
|
||||
const app = apps.find(a => appId(a) === id);
|
||||
const [showMetadata, setShowMetadata] = useState(true);
|
||||
const [showLocalInfo, setShowLocalInfo] = useState(true);
|
||||
const [mirrorStatuses, setMirrorStatuses] = useState<{ [mirror: string]: MirrorCheckFile | null }>({});
|
||||
const [selectedMirror, setSelectedMirror] = useState<string | null>(null);
|
||||
const [isDownloading, setIsDownloading] = useState(false);
|
||||
const [isInstalling, setIsInstalling] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [caps, setCaps] = useState<any>(null);
|
||||
const [showCaps, setShowCaps] = useState(false);
|
||||
const [localProgress, setLocalProgress] = useState<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (app) {
|
||||
checkMirrors();
|
||||
fetchCaps();
|
||||
}
|
||||
}, [app]);
|
||||
|
||||
if (!app) {
|
||||
return <div className="app-page"><h4>App details not found for {id}</h4></div>;
|
||||
}
|
||||
|
||||
const handleInstall = () => app && installApp(app);
|
||||
const handleUpdate = () => app && updateApp(app);
|
||||
const handleUninstall = () => app && uninstallApp(app);
|
||||
const handleMirror = () => app && setMirroring(app, !app.state?.mirroring);
|
||||
const handleApproveCaps = () => app && approveCaps(app);
|
||||
const handleAutoUpdate = () => app && setAutoUpdate(app, !app.state?.auto_update);
|
||||
const checkMirrors = async () => {
|
||||
const mirrors = [app.publisher, ...(app.metadata?.properties?.mirrors || [])];
|
||||
const statuses: { [mirror: string]: MirrorCheckFile | null } = {};
|
||||
for (const mirror of mirrors) {
|
||||
const status = await checkMirror(mirror);
|
||||
statuses[mirror] = status;
|
||||
}
|
||||
setMirrorStatuses(statuses);
|
||||
setSelectedMirror(statuses[app.publisher]?.is_online ? app.publisher : mirrors.find(m => statuses[m]?.is_online) || null);
|
||||
};
|
||||
|
||||
const fetchCaps = async () => {
|
||||
try {
|
||||
const appCaps = await getCaps(app);
|
||||
setCaps(appCaps);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch capabilities:', error);
|
||||
setError(`Failed to fetch capabilities: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownload = async () => {
|
||||
if (selectedMirror) {
|
||||
setError(null);
|
||||
setIsDownloading(true);
|
||||
setLocalProgress(0);
|
||||
try {
|
||||
await downloadApp(app, selectedMirror);
|
||||
setLocalProgress(100);
|
||||
setTimeout(() => {
|
||||
setIsDownloading(false);
|
||||
setLocalProgress(null);
|
||||
}, 3000);
|
||||
} catch (error) {
|
||||
console.error('Download failed:', error);
|
||||
setError(`Download failed: ${error instanceof Error ? error.message : String(error)}`);
|
||||
setIsDownloading(false);
|
||||
setLocalProgress(null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleInstall = async () => {
|
||||
setIsInstalling(true);
|
||||
setError(null);
|
||||
try {
|
||||
if (!caps?.approved) {
|
||||
await approveCaps(app);
|
||||
}
|
||||
await installApp(app);
|
||||
await getApp(app.package);
|
||||
} catch (error) {
|
||||
console.error('Installation failed:', error);
|
||||
setError(`Installation failed: ${error instanceof Error ? error.message : String(error)}`);
|
||||
} finally {
|
||||
setIsInstalling(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdate = () => updateApp(app);
|
||||
const handleUninstall = () => uninstallApp(app);
|
||||
const handleMirror = () => setMirroring(app, !app.state?.mirroring);
|
||||
const handleAutoUpdate = () => setAutoUpdate(app, !app.state?.auto_update);
|
||||
const handleLaunch = () => {
|
||||
console.log("Launching app:", app.package);
|
||||
window.open(`/${app.package}:${app.publisher}`, '_blank');
|
||||
window.open(`/${app.package}${app.package}:${app.publisher}`, '_blank');
|
||||
};
|
||||
|
||||
const isDownloaded = app.state !== undefined;
|
||||
const isInstalled = app.installed;
|
||||
|
||||
const handleCheckMirror = (mirror: string) => {
|
||||
setMirrorStatuses(prev => ({ ...prev, [mirror]: null })); // Set to loading
|
||||
checkMirror(mirror)
|
||||
.then(status => setMirrorStatuses(prev => ({ ...prev, [mirror]: status })))
|
||||
.catch(error => {
|
||||
console.error(`Failed to check mirror ${mirror}:`, error);
|
||||
setMirrorStatuses(prev => ({ ...prev, [mirror]: { node: mirror, is_online: false, error: "Request failed" } }));
|
||||
});
|
||||
};
|
||||
const progressPercentage = localProgress !== null
|
||||
? localProgress
|
||||
: isDownloaded ? 100 : 0;
|
||||
|
||||
return (
|
||||
<section className="app-page">
|
||||
@ -53,8 +121,8 @@ export default function AppPage() {
|
||||
|
||||
<div className="app-description">{app.metadata?.description || "No description available"}</div>
|
||||
|
||||
<div className="app-details">
|
||||
<div className="app-info">
|
||||
<div className="app-content">
|
||||
<div className="app-info-column">
|
||||
<div className="info-section">
|
||||
<h3 onClick={() => setShowMetadata(!showMetadata)}>
|
||||
Metadata {showMetadata ? <FaChevronUp /> : <FaChevronDown />}
|
||||
@ -67,27 +135,27 @@ export default function AppPage() {
|
||||
<li className="mirrors-list">
|
||||
<span>Mirrors:</span>
|
||||
<ul>
|
||||
{app.metadata?.properties?.mirrors?.map((mirror) => (
|
||||
{Object.entries(mirrorStatuses).map(([mirror, status]) => (
|
||||
<li key={mirror} className="mirror-item">
|
||||
<span className="mirror-address">{mirror}</span>
|
||||
<button
|
||||
onClick={() => handleCheckMirror(mirror)}
|
||||
onClick={() => checkMirror(mirror)}
|
||||
className="check-button"
|
||||
title="Check if mirror is online"
|
||||
>
|
||||
<FaSync className={mirrorStatuses[mirror] === null ? 'spinning' : ''} />
|
||||
<FaSync className={status === null ? 'spinning' : ''} />
|
||||
</button>
|
||||
{mirrorStatuses[mirror] && (
|
||||
{status && (
|
||||
<span className="mirror-status">
|
||||
{mirrorStatuses[mirror]?.is_online ? (
|
||||
{status.is_online ? (
|
||||
<><FaCheck className="online" /> <span className="online">Online</span></>
|
||||
) : (
|
||||
<>
|
||||
<FaTimes className="offline" />
|
||||
<span className="offline">Offline</span>
|
||||
{mirrorStatuses[mirror]?.error && (
|
||||
{status.error && (
|
||||
<span className="error-message">
|
||||
({mirrorStatuses[mirror]?.error})
|
||||
({status.error})
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
@ -109,7 +177,7 @@ export default function AppPage() {
|
||||
<ul className="detail-list">
|
||||
<li>
|
||||
<span>Installed:</span>
|
||||
<span className="status-icon">{app.installed ? <FaCheck className="installed" /> : <FaTimes className="not-installed" />}</span>
|
||||
<span className="status-icon">{isInstalled ? <FaCheck className="installed" /> : <FaTimes className="not-installed" />}</span>
|
||||
</li>
|
||||
<li><span>Installed Version:</span> <span>{app.state?.our_version || "Not installed"}</span></li>
|
||||
<li>
|
||||
@ -119,7 +187,7 @@ export default function AppPage() {
|
||||
<li><span>License:</span> <span>{app.metadata?.properties?.license || "Not specified"}</span></li>
|
||||
<li>
|
||||
<span>Capabilities Approved:</span>
|
||||
<button onClick={handleApproveCaps} className={`toggle-button ${app.state?.caps_approved ? 'active' : ''}`}>
|
||||
<button onClick={() => approveCaps(app)} className={`toggle-button ${app.state?.caps_approved ? 'active' : ''}`}>
|
||||
{app.state?.caps_approved ? <FaCheck /> : <FaShieldAlt />}
|
||||
{app.state?.caps_approved ? "Approved" : "Approve Caps"}
|
||||
</button>
|
||||
@ -143,21 +211,88 @@ export default function AppPage() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="app-actions">
|
||||
{app.installed ? (
|
||||
<>
|
||||
<button onClick={handleLaunch} className="primary"><FaRocket /> Launch</button>
|
||||
<button onClick={handleUpdate} className="secondary"><FaSync /> Update</button>
|
||||
<button onClick={handleUninstall} className="secondary"><FaTrash /> Uninstall</button>
|
||||
</>
|
||||
) : (
|
||||
<button onClick={handleInstall} className="primary"><FaDownload /> Install</button>
|
||||
|
||||
<div className="app-actions-column">
|
||||
<div className="app-actions">
|
||||
{isInstalled ? (
|
||||
<>
|
||||
<button onClick={handleLaunch} className="primary"><FaPlay /> Launch</button>
|
||||
<button onClick={handleUpdate} className="secondary"><FaSync /> Update</button>
|
||||
<button onClick={handleUninstall} className="secondary"><FaTrash /> Uninstall</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="mirror-selection">
|
||||
<select
|
||||
value={selectedMirror || ''}
|
||||
onChange={(e) => setSelectedMirror(e.target.value)}
|
||||
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)'}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<button
|
||||
className="download-button"
|
||||
onClick={handleDownload}
|
||||
disabled={!selectedMirror || isDownloading}
|
||||
>
|
||||
{isDownloading ? (
|
||||
<>
|
||||
<FaSpinner className="fa-spin" /> Downloading...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FaDownload /> {isDownloaded ? 'Re-download' : 'Download'}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
className="install-button"
|
||||
onClick={handleInstall}
|
||||
disabled={!isDownloaded || isInstalling}
|
||||
>
|
||||
<FaRocket /> {isInstalling ? 'Installing...' : 'Install'}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{app.metadata?.external_url && (
|
||||
<a href={app.metadata.external_url} target="_blank" rel="noopener noreferrer" className="external-link">
|
||||
<FaLink /> External Link
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{(isDownloading || isDownloaded) && (
|
||||
<div className="progress-container">
|
||||
<div className="progress-bar">
|
||||
<div
|
||||
className="progress"
|
||||
style={{ width: `${progressPercentage}%` }}
|
||||
></div>
|
||||
<div className="progress-percentage">{progressPercentage}%</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{app.metadata?.external_url && (
|
||||
<a href={app.metadata.external_url} target="_blank" rel="noopener noreferrer" className="external-link">
|
||||
<FaLink /> External Link
|
||||
</a>
|
||||
|
||||
{error && (
|
||||
<div className="error-message">
|
||||
<FaExclamationTriangle /> {error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="capabilities-section">
|
||||
<h3 onClick={() => setShowCaps(!showCaps)}>
|
||||
Requested Capabilities {showCaps ? <FaChevronUp /> : <FaChevronDown />}
|
||||
</h3>
|
||||
{showCaps && caps && (
|
||||
<pre className="capabilities">{JSON.stringify(caps, null, 2)}</pre>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -173,4 +308,5 @@ export default function AppPage() {
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
|
||||
}
|
@ -11,7 +11,7 @@ const BASE_URL = '/main:app_store:sys'
|
||||
interface AppsStore {
|
||||
apps: AppInfo[]
|
||||
ws: KinodeClientApi
|
||||
downloads: Map<string, [number, number]>
|
||||
downloads: Record<string, [number, number]>
|
||||
getApps: () => Promise<void>
|
||||
getApp: (id: string) => Promise<AppInfo>
|
||||
checkMirror: (node: string) => Promise<MirrorCheckFile>
|
||||
@ -31,7 +31,7 @@ const useAppsStore = create<AppsStore>()(
|
||||
(set, get) => ({
|
||||
apps: [],
|
||||
|
||||
downloads: new Map(),
|
||||
downloads: {},
|
||||
|
||||
ws: new KinodeClientApi({
|
||||
uri: WEBSOCKET_URL,
|
||||
@ -39,14 +39,19 @@ const useAppsStore = create<AppsStore>()(
|
||||
processId: "main:app_store:sys",
|
||||
onMessage: (message) => {
|
||||
const data = JSON.parse(message);
|
||||
console.log('we got a json message', data)
|
||||
if (data.kind === 'progress') {
|
||||
const appId = data.data.name.split('/').pop().split('.').shift();
|
||||
set((state) => {
|
||||
const newDownloads = new Map(state.downloads);
|
||||
newDownloads.set(appId, [data.data.chunks_received, data.data.total_chunks]);
|
||||
return { downloads: newDownloads };
|
||||
});
|
||||
const appId = data.data.file_name.slice(1).replace('.zip', '');
|
||||
console.log('got app id with progress: ', appId, data.data.chunks_received, data.data.total_chunks)
|
||||
set((state) => ({
|
||||
downloads: {
|
||||
...state.downloads,
|
||||
[appId]: [data.data.chunks_received, data.data.total_chunks]
|
||||
}
|
||||
}));
|
||||
|
||||
if (data.data.chunks_received === data.data.total_chunks) {
|
||||
get().getApp(appId);
|
||||
}
|
||||
}
|
||||
},
|
||||
onOpen: (_e) => {
|
||||
@ -81,7 +86,7 @@ const useAppsStore = create<AppsStore>()(
|
||||
},
|
||||
|
||||
installApp: async (app: AppInfo) => {
|
||||
const res = await fetch(`${BASE_URL}/apps/${appId(app)}`, { method: 'POST' })
|
||||
const res = await fetch(`${BASE_URL}/apps/${appId(app)}/install`, { method: 'POST' })
|
||||
if (res.status !== HTTP_STATUS.CREATED) {
|
||||
throw new Error(`Failed to install app: ${appId(app)}`)
|
||||
}
|
||||
@ -89,11 +94,8 @@ const useAppsStore = create<AppsStore>()(
|
||||
},
|
||||
|
||||
updateApp: async (app: AppInfo) => {
|
||||
const res = await fetch(`${BASE_URL}/apps/${appId(app)}`, { method: 'PUT' })
|
||||
if (res.status !== HTTP_STATUS.CREATED) {
|
||||
throw new Error(`Failed to update app: ${appId(app)}`)
|
||||
}
|
||||
await get().getApp(appId(app))
|
||||
// Note: The backend doesn't have a specific update endpoint, so we might need to implement this differently
|
||||
throw new Error('Update functionality not implemented')
|
||||
},
|
||||
|
||||
uninstallApp: async (app: AppInfo) => {
|
||||
@ -105,8 +107,8 @@ const useAppsStore = create<AppsStore>()(
|
||||
},
|
||||
|
||||
downloadApp: async (app: AppInfo, downloadFrom: string) => {
|
||||
const res = await fetch(`${BASE_URL}/apps/${appId(app)}`, {
|
||||
method: 'POST',
|
||||
const res = await fetch(`${BASE_URL}/apps/${appId(app)}/download`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ download_from: downloadFrom }),
|
||||
})
|
||||
if (res.status !== HTTP_STATUS.CREATED) {
|
||||
|
Loading…
Reference in New Issue
Block a user