app_store: new download flow

This commit is contained in:
bitful-pannul 2024-08-01 17:48:21 +03:00
parent ca0996aca7
commit c47e41a87b
7 changed files with 424 additions and 265 deletions

View File

@ -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);
}
}
}

View File

@ -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)?);

View File

@ -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,

View 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("/", "");

View File

@ -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;
}

View File

@ -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>
);
}

View File

@ -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) {