Skip to content
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
94 changes: 94 additions & 0 deletions kinode/packages/app_store/pkg/ui/assets/index-BxGs27ah.js

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion kinode/packages/app_store/pkg/ui/assets/index-i4SytJ9j.css

This file was deleted.

94 changes: 0 additions & 94 deletions kinode/packages/app_store/pkg/ui/assets/index-kpw1YN6W.js

This file was deleted.

1 change: 1 addition & 0 deletions kinode/packages/app_store/pkg/ui/assets/index-umttRNrr.css

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions kinode/packages/app_store/pkg/ui/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport"
content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1.00001, viewport-fit=cover" />
<script type="module" crossorigin src="/main:app_store:sys/assets/index-kpw1YN6W.js"></script>
<link rel="stylesheet" crossorigin href="/main:app_store:sys/assets/index-i4SytJ9j.css">
<script type="module" crossorigin src="/main:app_store:sys/assets/index-BxGs27ah.js"></script>
<link rel="stylesheet" crossorigin href="/main:app_store:sys/assets/index-umttRNrr.css">
</head>

<body>
Expand Down
4 changes: 2 additions & 2 deletions kinode/packages/app_store/ui/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "kit-ui",
"name": "kinode-app-store",
"private": true,
"version": "0.0.0",
"type": "module",
Expand Down Expand Up @@ -54,4 +54,4 @@
"typescript": "^5.2.2",
"vite": "^5.0.8"
}
}
}
2 changes: 1 addition & 1 deletion kinode/packages/app_store/ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ function App() {
const props = { provider, packageAbi };

return (
<div className="flex flex-col c h-screen w-screen">
<div className="flex flex-col c h-screen w-screen max-h-screen max-w-screen overflow-x-hidden special-appstore-background">
<Web3ReactProvider connectors={connectors}>
<Router basename={BASE_URL}>
<Routes>
Expand Down
255 changes: 38 additions & 217 deletions kinode/packages/app_store/ui/src/components/ActionButton.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,19 @@
import React, { FormEvent, useCallback, useEffect, useMemo, useState } from "react";
import React, { useEffect, useMemo, useState } from "react";
import { AppInfo } from "../types/Apps";
import useAppsStore from "../store/apps-store";
import Modal from "./Modal";
import { getAppName } from "../utils/app";
import Loader from "./Loader";
import classNames from "classnames";
import { useNavigate } from "react-router-dom";
import UpdateButton from "./UpdateButton";
import DownloadButton from "./DownloadButton";
import InstallButton from "./InstallButton";
import LaunchButton from "./LaunchButton";
import { FaCheck } from "react-icons/fa6";

interface ActionButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
app: AppInfo;
isIcon?: boolean;
}

export default function ActionButton({ app, ...props }: ActionButtonProps) {
const { updateApp, downloadApp, installApp, getCaps, getMyApp } =
useAppsStore();
const [showModal, setShowModal] = useState(false);
const [mirror, setMirror] = useState(app.metadata?.properties?.mirrors?.[0] || "Other");
const [customMirror, setCustomMirror] = useState("");
const [caps, setCaps] = useState<string[]>([]);
const [launchPath, setLaunchPath] = useState('');
const [loading, setLoading] = useState("");

const { clean, installed, downloaded, updatable } = useMemo(() => {
export default function ActionButton({ app, isIcon = false, ...props }: ActionButtonProps) {
const [incrementNumber, setIncrementNumber] = useState(0);
const { installed, downloaded, updatable } = useMemo(() => {
const versions = Object.entries(app?.metadata?.properties?.code_hashes || {});
const latestHash = (versions.find(([v]) => v === app.metadata?.properties?.current_version) || [])[1];

Expand All @@ -31,217 +23,46 @@ export default function ActionButton({ app, ...props }: ActionButtonProps) {
const updatable =
Boolean(app.state?.our_version && latestHash) &&
app.state?.our_version !== latestHash &&
app.publisher !== window.our.node;
app.publisher !== (window as any).our.node;
return {
clean: !installed && !downloaded && !updatable,
installed,
downloaded,
updatable,
};
}, [app]);
}, [app, incrementNumber]);

useEffect(() => {
setMirror(app.metadata?.properties?.mirrors?.[0] || "Other");
}, [app.metadata?.properties?.mirrors]);

const [launchPath, setLaunchPath] = useState('');

useEffect(() => {
if (installed) {
fetch('/apps').then(data => data.json())
.then((data: Array<{ package_name: string, path: string }>) => {
// console.log(data)
if (Array.isArray(data)) {
// console.log('is array')
const homepageAppData = data.find(otherApp => app.package === otherApp.package_name)
if (homepageAppData) {
// console.log('found the good appness', homepageAppData.package_name, homepageAppData.path);
setLaunchPath(homepageAppData.path)
}
fetch('/apps').then(data => data.json())
.then((data: Array<{ package_name: string, path: string }>) => {
if (Array.isArray(data)) {
const homepageAppData = data.find(otherApp => app.package === otherApp.package_name)
if (homepageAppData) {
setLaunchPath(homepageAppData.path)
}
})
}
}, [installed])

const onClick = useCallback(async () => {
if (installed && !updatable && launchPath) {
window.location.href = `/${launchPath.replace('/', '')}`
return;
} else {
if (downloaded) {
getCaps(app).then((manifest) => {
setCaps(manifest.request_capabilities);
});
}
setShowModal(true);
}
}, [app, installed, downloaded, updatable, setShowModal, getCaps, launchPath]);

const download = useCallback(async (e: FormEvent) => {
e.preventDefault();
e.stopPropagation();
const targetMirror = mirror === "Other" ? customMirror : mirror;

if (!targetMirror) {
window.alert("Please select a mirror");
return;
}

try {
setLoading(`Downloading ${getAppName(app)}...`);
await downloadApp(app, targetMirror);
const interval = setInterval(() => {
getMyApp(app)
.then(() => {
setLoading("");
setShowModal(false);
clearInterval(interval);
})
.catch(console.log);
}, 2000);
} catch (e) {
console.error(e);
window.alert(
`Failed to download app from ${targetMirror}, please try a different mirror.`
);
setLoading("");
}
}, [mirror, customMirror, app, downloadApp, getMyApp]);

const install = useCallback(async () => {
try {
setLoading(`Installing ${getAppName(app)}...`);
await installApp(app);

const interval = setInterval(() => {
getMyApp(app)
.then((app) => {
if (!app.installed) return;
setLoading("");
setShowModal(false);
clearInterval(interval);
})
.catch(console.log);
}, 2000);
} catch (e) {
console.error(e);
window.alert(`Failed to install, please try again.`);
setLoading("");
}
}, [app, installApp, getMyApp]);

const update = useCallback(async () => {
try {
setLoading(`Updating ${getAppName(app)}...`);
await updateApp(app);

const interval = setInterval(() => {
getMyApp(app)
.then((app) => {
if (!app.installed) return;
setLoading("");
setShowModal(false);
clearInterval(interval);
})
.catch(console.log);
}, 2000);
} catch (e) {
console.error(e);
window.alert(`Failed to update, please try again.`);
setLoading("");
}
}, [app, updateApp, getMyApp]);

const appName = getAppName(app);
}
})
}, [app, incrementNumber])

return (
<>
<button
{...props}
type="button"
className={classNames("text-sm min-w-[100px] px-2 py-1 self-start", props.className, {
'bg-orange': installed,
'hidden': installed && !updatable && !launchPath
})}
onClick={onClick}
>
{installed && updatable
? "Update"
: installed && launchPath
? "Launch"
: installed
? "Installed"
: downloaded
? "Install"
: "Download"}
</button>
<Modal show={showModal} hide={() => setShowModal(false)}>
{loading ? (
<Loader msg={loading} />
) : clean ? (
<form className="flex flex-col items-center gap-2" onSubmit={download}>
<h4>Download '{appName}'</h4>
<h5 style={{ margin: 0 }}>Select Mirror</h5>
<select value={mirror} onChange={(e) => setMirror(e.target.value)}>
{((app.metadata?.properties?.mirrors || []).concat(["Other"])).map((m) => (
<option key={m} value={m}>
{m}
</option>
))}
</select>
{mirror === "Other" && (
<input
type="text"
value={customMirror}
onChange={(e) => setCustomMirror(e.target.value)}
placeholder="Mirror, i.e. 'template.os'"
className="p-1 max-w-[240px] w-full"
required
autoFocus
/>
)}
<button type="submit">
Download
</button>
</form>
) : downloaded ? (
<>
<h4>Approve App Permissions</h4>
<h5 className="m-0">
{getAppName(app)} needs the following permissions:
</h5>
<ul className="flex flex-col items-start">
{caps.map((cap) => (
<li key={cap}>{cap}</li>
))}
</ul>
<button type="button" onClick={install}>
Approve & Install
</button>
</>
) : (
<>
<h4>Approve App Permissions</h4>
<h5 className="m-0">
{getAppName(app)} needs the following permissions:
</h5>
{/* <h5>Send Messages:</h5> */}
<br />
<ul className="flex flex-col items-start">
{caps.map((cap) => (
<li key={cap}>{cap}</li>
))}
</ul>
{/* <h5>Receive Messages:</h5>
<ul>
{caps.map((cap) => (
<li key={cap}>{cap}</li>
))}
</ul> */}
<button type="button" onClick={update}>
Approve & Update
</button>
</>
)}
</Modal>
{(installed && launchPath)
? <LaunchButton app={app} {...props} isIcon={isIcon} launchPath={launchPath} />
: (installed && updatable)
? <UpdateButton app={app} {...props} isIcon={isIcon} callback={() => setIncrementNumber(incrementNumber + 1)} />
: !downloaded
? <DownloadButton app={app} {...props} isIcon={isIcon} callback={() => setIncrementNumber(incrementNumber + 1)} />
: !installed
? <InstallButton app={app} {...props} isIcon={isIcon} callback={() => setIncrementNumber(incrementNumber + 1)} />
: isIcon
? <button
className="pointer-events none icon clear absolute top-0 right-0"
>
<FaCheck />
</button>
: <div>Installed</div>}
</>
);
}
35 changes: 25 additions & 10 deletions kinode/packages/app_store/ui/src/components/AppEntry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,40 @@ import AppHeader from "./AppHeader";
import ActionButton from "./ActionButton";
import { AppInfo } from "../types/Apps";
import { appId } from "../utils/app";
import MoreActions from "./MoreActions";
import { isMobileCheck } from "../utils/dimensions";
import classNames from "classnames";
import { useNavigate } from "react-router-dom";
import { APP_DETAILS_PATH } from "../constants/path";

interface AppEntryProps extends React.HTMLAttributes<HTMLDivElement> {
app: AppInfo;
size?: "small" | "medium" | "large";
overrideImageSize?: "small" | "medium" | "large";
}

export default function AppEntry({ app, ...props }: AppEntryProps) {
export default function AppEntry({ app, size = "medium", overrideImageSize, ...props }: AppEntryProps) {
const isMobile = isMobileCheck()
const navigate = useNavigate()

return (
<div {...props} key={appId(app)} className={classNames("flex justify-between w-full rounded hover:bg-white/10 card", {
'flex-wrap gap-2': isMobile
})}>
<AppHeader app={app} size="small" />
<div className="flex mr-1 items-start">
<ActionButton app={app} className="mr-2" />
<MoreActions app={app} />
</div>
<div
{...props}
key={appId(app)}
className={classNames("flex justify-between rounded-lg hover:bg-white/10 card cursor-pointer", props.className, {
'flex-wrap gap-2': isMobile,
'flex-col relative': size !== 'large'
})}
onClick={() => navigate(`/${APP_DETAILS_PATH}/${appId(app)}`)}
>
<AppHeader app={app} size={size} overrideImageSize={overrideImageSize} />
<ActionButton
app={app}
isIcon={size !== 'large'}
className={classNames({
'absolute top-0 right-0': size !== 'large',
'bg-orange text-lg min-w-1/5': size === 'large',
'ml-auto': size === 'large' && isMobile
})} />
</div>
);
}
Loading