Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

app_store: fix launch button #663

Merged
merged 4 commits into from
Jan 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions kinode/packages/app-store/app-store/src/http_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ pub fn init_frontend(our: &Address, http_server: &mut server::HttpServer) {
"/installed", // all installed apps
"/ourapps", // all apps we've published
"/updates", // all auto_updates
"/homepageapps", // all apps on homepage
"/apps/:id", // detail about an on-chain app
"/downloads/:id", // local downloads for an app
"/installed/:id", // detail about an installed app
Expand Down Expand Up @@ -482,6 +483,14 @@ fn serve_paths(
_ => Err(anyhow::anyhow!("Invalid response from chain: {:?}", msg)),
}
}
"/homepageapps" => {
let resp = Request::to(("our", "homepage", "homepage", "sys"))
.body(serde_json::to_vec(&"GetApps")?)
.send_and_await_response(5)??;

// todo: import homepage with and parse into proper response type?
Ok((StatusCode::OK, None, resp.body().to_vec()))
}
// POST /apps/:id/download
// download a listed app from a mirror
"/apps/:id/download" => {
Expand Down
1 change: 1 addition & 0 deletions kinode/packages/app-store/pkg/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
"http-server:distro:sys",
"kns-indexer:kns-indexer:sys",
"terminal:terminal:sys",
"homepage:homepage:sys",
"vfs:distro:sys"
],
"public": false
Expand Down
12 changes: 5 additions & 7 deletions kinode/packages/app-store/ui/src/pages/AppPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -174,13 +174,11 @@ export default function AppPage() {
<div className="app-actions">
{installedApp && (
<>
<button
onClick={handleLaunch}
className="primary"
disabled={!canLaunch}
>
<FaPlay /> {canLaunch ? 'Launch' : 'No UI found for app'}
</button>
{canLaunch && (
<button onClick={handleLaunch} className="primary">
<FaPlay /> Launch
</button>
)}
<button onClick={handleUninstall} className="secondary" disabled={isUninstalling}>
{isUninstalling ? <FaSpinner className="fa-spin" /> : <FaTrash />} Uninstall
</button>
Expand Down
91 changes: 40 additions & 51 deletions kinode/packages/app-store/ui/src/pages/DownloadPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export default function DownloadPage() {
clearAllActiveDownloads,
fetchHomepageApps,
getLaunchUrl,
homepageApps,
} = useAppsStore();

const [showMetadata, setShowMetadata] = useState(false);
Expand All @@ -29,8 +30,7 @@ export default function DownloadPage() {
const [showCapApproval, setShowCapApproval] = useState(false);
const [manifestResponse, setManifestResponse] = useState<ManifestResponse | null>(null);
const [isInstalling, setIsInstalling] = useState(false);
const [isCheckingLaunch, setIsCheckingLaunch] = useState(false);
const [launchPath, setLaunchPath] = useState<string | null>(null);
const [isPolling, setIsPolling] = useState(false);

const app = useMemo(() => listings[id || ""], [listings, id]);
const appDownloads = useMemo(() => downloads[id || ""] || [], [downloads, id]);
Expand Down Expand Up @@ -70,6 +70,24 @@ export default function DownloadPage() {
}
}, [sortedVersions, selectedVersion]);

useEffect(() => {
if (isPolling) {
const pollInterval = setInterval(() => {
fetchHomepageApps();
}, 1000);

// Stop polling after 5 seconds
const timeout = setTimeout(() => {
setIsPolling(false);
}, 5000);

return () => {
clearInterval(pollInterval);
clearTimeout(timeout);
};
}
}, [isPolling, fetchHomepageApps]);

const isDownloaded = useMemo(() => {
if (!app || !selectedVersion) return false;
const versionData = sortedVersions.find(v => v.version === selectedVersion);
Expand Down Expand Up @@ -104,35 +122,19 @@ export default function DownloadPage() {
return versionData ? installedApp.our_version_hash === versionData.hash : false;
}, [app, selectedVersion, installedApp, sortedVersions]);

const checkLaunchPath = useCallback(() => {
if (!app) return;
setIsCheckingLaunch(true);
const appId = `${app.package_id.package_name}:${app.package_id.publisher_node}`;
fetchHomepageApps().then(() => {
const path = getLaunchUrl(appId);
setLaunchPath(path);
setIsCheckingLaunch(false);
if (path) {
setIsInstalling(false);
}
});
}, [app, fetchHomepageApps, getLaunchUrl]);

useEffect(() => {
if (isInstalling) {
const checkInterval = setInterval(checkLaunchPath, 500);
const timeout = setTimeout(() => {
clearInterval(checkInterval);
setIsInstalling(false);
setIsCheckingLaunch(false);
}, 5000);
const canLaunch = useMemo(() => {
if (!app) return false;
return !!getLaunchUrl(`${app.package_id.package_name}:${app.package_id.publisher_node}`);
}, [app, getLaunchUrl, homepageApps]);

return () => {
clearInterval(checkInterval);
clearTimeout(timeout);
};
const handleLaunch = useCallback(() => {
if (app) {
const launchUrl = getLaunchUrl(`${app.package_id.package_name}:${app.package_id.publisher_node}`);
if (launchUrl) {
window.location.href = window.location.origin.replace('//app-store-sys.', '//') + launchUrl;
}
}
}, [isInstalling, checkLaunchPath]);
}, [app, getLaunchUrl]);

const handleDownload = useCallback(() => {
if (!id || !selectedMirror || !app || !selectedVersion) return;
Expand Down Expand Up @@ -172,40 +174,31 @@ export default function DownloadPage() {
const versionData = sortedVersions.find(v => v.version === selectedVersion);
if (versionData) {
setIsInstalling(true);
setLaunchPath(null);
installApp(id, versionData.hash).then(() => {
setShowCapApproval(false);
setManifestResponse(null);
fetchData(id);
fetchHomepageApps();
setIsPolling(true); // Start polling
setIsInstalling(false);
});
}
}, [id, selectedVersion, sortedVersions, installApp, fetchData]);

const handleLaunch = useCallback(() => {
if (launchPath) {
window.location.href = window.location.origin.replace('//app-store-sys.', '//') + launchPath;
}
}, [launchPath]);

const canLaunch = useMemo(() => {
if (!app) return false;
return !!getLaunchUrl(`${app.package_id.package_name}:${app.package_id.publisher_node}`);
}, [app, getLaunchUrl]);
}, [id, selectedVersion, sortedVersions, installApp, fetchData, fetchHomepageApps]);

const canDownload = useMemo(() => {
return selectedMirror && (isMirrorOnline === true || selectedMirror.startsWith('http')) && !isDownloading && !isDownloaded;
}, [selectedMirror, isMirrorOnline, isDownloading, isDownloaded]);

const renderActionButton = () => {
if (isCurrentVersionInstalled || launchPath) {
if (isCurrentVersionInstalled) {
return (
<button className="action-button installed-button" disabled>
<FaRocket /> Installed
</button>
);
}

if (isInstalling || isCheckingLaunch) {
if (isInstalling) {
return (
<button className="action-button installing-button" disabled>
<FaSpinner className="fa-spin" /> Installing...
Expand Down Expand Up @@ -264,20 +257,16 @@ export default function DownloadPage() {
<p className="app-id">{`${app.package_id.package_name}.${app.package_id.publisher_node}`}</p>
</div>
</div>
{launchPath ? (
{canLaunch ? (
<button
onClick={handleLaunch}
className="launch-button"
>
<FaPlay /> Launch
</button>
) : isInstalling || isCheckingLaunch ? (
<button className="launch-button" disabled>
<FaSpinner className="fa-spin" /> Checking...
</button>
) : installedApp ? (
) : (isInstalling || isPolling) ? (
<button className="launch-button" disabled>
No UI found for app
<FaSpinner className="fa-spin" /> {isInstalling ? 'Installing...' : 'Checking...'}
</button>
) : null}
</div>
Expand Down
15 changes: 7 additions & 8 deletions kinode/packages/app-store/ui/src/store/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,22 +190,21 @@ const useAppsStore = create<AppsStore>()((set, get) => ({

fetchHomepageApps: async () => {
try {
const res = await fetch('/apps');
const res = await fetch(`${BASE_URL}/homepageapps`);
if (res.status === HTTP_STATUS.OK) {
const data: HomepageApp[] = await res.json();
set({ homepageApps: data });
const data = await res.json();
const apps = data.GetApps || [];
set({ homepageApps: apps });
}
} catch (error) {
console.error("Error fetching homepage apps:", error);
set({ homepageApps: [] });
}
},

getLaunchUrl: (id: string) => {
const app = get().homepageApps.find(app => `${app.package}:${app.publisher}` === id);
if (app && app.path) {
return app.path;
}
return null;
const app = get().homepageApps?.find(app => `${app.package_name}:${app.publisher}` === id);
return app?.path || null;
},

checkMirror: async (id: string, node: string) => {
Expand Down
2 changes: 1 addition & 1 deletion kinode/packages/app-store/ui/src/types/Apps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export interface ManifestResponse {
export interface HomepageApp {
id: string;
process: string;
package: string;
package_name: string;
publisher: string;
path?: string;
label: string;
Expand Down
22 changes: 22 additions & 0 deletions kinode/packages/homepage/api/homepage:sys-v1.wit
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,15 @@ interface homepage {
///
/// lazy-load-blob: none.
set-stylesheet(string),
/// get the list of apps currently on the homepage
///
/// lazy-load-blob: none.
get-apps,
}

/// The response format to get from the homepage. Serialized using serde_json.
variant response {
get-apps(list<app>),
}

record add-request {
Expand All @@ -37,6 +46,19 @@ interface homepage {
path: option<string>,
widget: option<string>,
}

record app {
id: string,
process: string,
package-name: string,
publisher: string,
path: option<string>,
label: string,
base64-icon: option<string>,
widget: option<string>,
order: u32,
favorite: bool,
}
}

world homepage-sys-v1 {
Expand Down
38 changes: 16 additions & 22 deletions kinode/packages/homepage/homepage/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use crate::kinode::process::homepage;
use kinode_process_lib::{
await_message, call_init, get_blob, http, http::server, println, Address, Capability,
LazyLoadBlob,
await_message, call_init, get_blob,
http::{self, server},
println, Address, Capability, LazyLoadBlob, Response,
};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, HashMap};

/// Fetching OS version from main package
Expand All @@ -15,20 +15,6 @@ const DEFAULT_FAVES: &[&str] = &[
"settings:settings:sys",
];

#[derive(Serialize, Deserialize)]
struct HomepageApp {
id: String,
process: String,
package: String,
publisher: String,
path: Option<String>,
label: String,
base64_icon: Option<String>,
widget: Option<String>,
order: u32,
favorite: bool, // **not currently used on frontend**
}

type PersistedAppOrder = HashMap<String, u32>;

wit_bindgen::generate!({
Expand All @@ -42,7 +28,7 @@ call_init!(init);
fn init(our: Address) {
println!("started");

let mut app_data: BTreeMap<String, HomepageApp> = BTreeMap::new();
let mut app_data: BTreeMap<String, homepage::App> = BTreeMap::new();

let mut http_server = server::HttpServer::new(5);
let http_config = server::HttpBindingConfig::default();
Expand Down Expand Up @@ -190,7 +176,7 @@ fn init(our: Address) {
Some(LazyLoadBlob::new(
Some("application/json"),
serde_json::to_vec(
&app_data.values().collect::<Vec<&HomepageApp>>(),
&app_data.values().collect::<Vec<&homepage::App>>(),
)
.unwrap(),
)),
Expand Down Expand Up @@ -284,7 +270,7 @@ fn init(our: Address) {
);
}
} else {
// handle messages to add or remove an app from the homepage.
// handle messages to get apps, add or remove an app from the homepage.
// they must have messaging access to us in order to perform this.
if let Ok(request) = serde_json::from_slice::<homepage::Request>(message.body()) {
match request {
Expand All @@ -297,10 +283,10 @@ fn init(our: Address) {
let id = message.source().process.to_string();
app_data.insert(
id.clone(),
HomepageApp {
homepage::App {
id: id.clone(),
process: message.source().process().to_string(),
package: message.source().package().to_string(),
package_name: message.source().package().to_string(),
publisher: message.source().publisher().to_string(),
path: path.map(|path| {
format!(
Expand Down Expand Up @@ -340,6 +326,14 @@ fn init(our: Address) {
app_data.remove(&id);
persisted_app_order.remove(&id);
}
homepage::Request::GetApps => {
let apps = app_data.values().cloned().collect::<Vec<homepage::App>>();
let resp = homepage::Response::GetApps(apps);
Response::new()
.body(serde_json::to_vec(&resp).unwrap())
.send()
.unwrap();
}
homepage::Request::SetStylesheet(new_stylesheet_string) => {
// caps check
let required_capability = Capability::new(
Expand Down
Loading