Merge pull request #547 from kinode-dao/bp/launchbuttons

app_store UI fixes
This commit is contained in:
bitful-pannul 2024-09-18 17:33:08 +03:00 committed by GitHub
commit 31f73db0b0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 187 additions and 304 deletions

View File

@ -83,6 +83,7 @@ form {
.form-group { .form-group {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
margin-bottom: 1rem;
} }
label { label {
@ -94,6 +95,8 @@ select {
padding: 0.5rem; padding: 0.5rem;
border: 1px solid var(--gray); border: 1px solid var(--gray);
border-radius: var(--border-radius); border-radius: var(--border-radius);
background-color: light-dark(var(--white), var(--tasteful-dark));
color: light-dark(var(--off-black), var(--off-white));
} }
/* Buttons */ /* Buttons */
@ -201,6 +204,12 @@ td {
flex-direction: column; flex-direction: column;
} }
.app-title-container {
display: flex;
align-items: center;
gap: 1rem;
}
.app-id { .app-id {
font-size: 0.9rem; font-size: 0.9rem;
color: var(--gray); color: var(--gray);
@ -228,277 +237,124 @@ td {
margin-top: 0.25rem; margin-top: 0.25rem;
} }
.form-group { /* App Page and Download Page shared styles */
margin-bottom: 1rem; .app-page,
}
/* App Page */
.app-page {
background-color: light-dark(var(--off-white), var(--off-black));
border-radius: var(--border-radius);
padding: 1rem;
margin-bottom: 1rem;
}
.app-description {
margin-bottom: 1rem;
}
.app-info {
background-color: light-dark(var(--tan), var(--tasteful-dark));
border-radius: var(--border-radius);
padding: 1rem;
margin-bottom: 1rem;
}
.app-actions {
display: flex;
gap: 1rem;
margin-bottom: 1rem;
}
.app-screenshots {
margin-top: 1rem;
}
.screenshot-container {
display: flex;
gap: 1rem;
overflow-x: auto;
}
.app-screenshot {
max-width: 200px;
height: auto;
}
/* Store Page */
.store-page {
background-color: light-dark(var(--off-white), var(--off-black));
border-radius: var(--border-radius);
padding: 1rem;
}
.store-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.app-list {
background-color: light-dark(var(--tan), var(--tasteful-dark));
border-radius: var(--border-radius);
padding: 1rem;
}
/* Publish Page */
.publish-page {
background-color: light-dark(var(--off-white), var(--off-black));
border-radius: var(--border-radius);
padding: 1rem;
}
.publish-form {
max-width: 500px;
}
.my-packages {
margin-top: 2rem;
}
.my-packages ul {
list-style-type: none;
padding: 0;
}
.my-packages li {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem 0;
border-bottom: 1px solid var(--gray);
}
/* Download Page */
.downloads-page { .downloads-page {
background-color: light-dark(var(--white), var(--maroon)); background-color: light-dark(var(--white), var(--maroon));
border-radius: 8px; border-radius: var(--border-radius);
padding: 2rem; padding: 2rem;
margin-bottom: 2rem; width: 100%;
} }
.app-header { .app-header {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 1rem; gap: 1rem;
margin-bottom: 1rem; margin-bottom: 1.5rem;
} }
.app-header h2 { .app-description {
margin: 0; margin-bottom: 2rem;
line-height: 1.6;
} }
.launch-button { .app-info {
display: inline-flex; background-color: light-dark(var(--tan), var(--tasteful-dark));
align-items: center; border-radius: var(--border-radius);
gap: 0.5rem; padding: 1.5rem;
padding: 0.5rem 1rem; margin-bottom: 2rem;
font-size: 14px;
background-color: var(--orange);
color: var(--white);
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s ease;
}
.launch-button:hover {
background-color: var(--dark-orange);
}
.version-selector {
margin-bottom: 1rem;
}
.version-selector select {
width: 100%;
padding: 0.75rem;
border: 2px solid var(--orange);
border-radius: 4px;
background-color: light-dark(var(--white), var(--tasteful-dark));
color: light-dark(var(--off-black), var(--off-white));
transition: all 0.3s ease;
}
.version-selector select:focus {
outline: none;
border-color: var(--dark-orange);
box-shadow: 0 0 0 3px rgba(255, 79, 0, 0.2);
} }
/* Download Page specific styles */
.download-section { .download-section {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1rem; gap: 1rem;
margin-bottom: 1rem; margin-bottom: 2rem;
max-width: 20rem;
} }
.download-button, .version-selector,
.install-button, .mirror-selector select {
.installed-button { width: 100%;
padding: 0.5em;
border: 1px solid var(--gray);
border-radius: var(--border-radius);
background-color: light-dark(var(--white), var(--tasteful-dark));
color: light-dark(var(--off-black), var(--off-white));
}
/* Action Buttons */
.action-button,
.primary,
.secondary {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 0.5rem; gap: 0.5rem;
padding: 0.5rem 1rem; padding: 0.75em 1em;
font-size: 16px; font-size: 1rem;
font-weight: bold;
border: none; border: none;
border-radius: 4px; border-radius: var(--border-radius);
cursor: pointer; cursor: pointer;
transition: background-color 0.3s ease; transition: background-color 0.3s ease, color 0.3s ease;
} }
.download-button { .primary {
background-color: var(--orange); background-color: var(--orange);
color: var(--white); color: var(--white);
} }
.download-button:hover:not(:disabled) { .primary:hover:not(:disabled) {
background-color: var(--dark-orange); background-color: var(--dark-orange);
}
.install-button {
background-color: var(--blue);
color: var(--white); color: var(--white);
} }
.install-button:hover { .secondary {
background-color: color-mix(in srgb, var(--blue) 80%, black); background-color: light-dark(var(--off-white), var(--off-black));
color: var(--orange);
border: 2px solid var(--orange);
} }
.installed-button { .secondary:hover:not(:disabled) {
background-color: var(--gray); background-color: var(--orange);
color: var(--white); color: var(--white);
cursor: not-allowed;
} }
.download-button:disabled, .action-button:disabled,
.install-button:disabled, .primary:disabled,
.installed-button:disabled { .secondary:disabled {
opacity: 0.5; opacity: 0.5;
cursor: not-allowed; cursor: not-allowed;
} }
.my-downloads { /* App actions */
margin-top: 1rem; .app-actions {
}
.my-downloads>button {
display: flex; display: flex;
align-items: center; gap: 1rem;
gap: 0.5rem; flex-wrap: wrap;
background-color: transparent; margin-bottom: 2rem;
color: var(--orange);
border: 2px solid var(--orange);
padding: 0.5rem 1rem;
cursor: pointer;
transition: background-color 0.3s ease, color 0.3s ease;
} }
.my-downloads>button:hover { /* Screenshots */
background-color: var(--orange); .app-screenshots {
color: var(--white); margin-top: 2rem;
} }
.my-downloads table { .screenshot-container {
width: 100%;
border-collapse: collapse;
margin-top: 1rem;
}
.my-downloads th,
.my-downloads td {
padding: 0.5rem;
text-align: left;
border-bottom: 1px solid var(--gray);
}
.my-downloads td button {
margin-right: 0.5rem;
}
.app-details {
margin-top: 1rem;
}
.app-details>button {
display: flex; display: flex;
align-items: center; gap: 1rem;
gap: 0.5rem;
background-color: transparent;
color: var(--orange);
border: 2px solid var(--orange);
padding: 0.5rem 1rem;
cursor: pointer;
transition: background-color 0.3s ease, color 0.3s ease;
}
.app-details>button:hover {
background-color: var(--orange);
color: var(--white);
}
.app-details pre {
background-color: light-dark(var(--tan), var(--tasteful-dark));
color: light-dark(var(--off-black), var(--off-white));
padding: 1rem;
border-radius: 4px;
overflow-x: auto; overflow-x: auto;
margin-top: 1rem; padding-bottom: 1rem;
} }
.app-screenshot {
max-width: 200px;
height: auto;
border-radius: var(--border-radius);
}
/* Capabilities approval popup */
.cap-approval-popup { .cap-approval-popup {
position: fixed; position: fixed;
top: 0; top: 0;
@ -538,73 +394,23 @@ td {
margin-top: 1rem; margin-top: 1rem;
} }
/* My Downloads Page */ /* Responsive adjustments */
.my-downloads-page { @media (max-width: 48em) {
background-color: light-dark(var(--off-white), var(--off-black));
border-radius: var(--border-radius); .app-page,
padding: 1rem; .downloads-page {
} padding: 1.5rem;
}
/* Responsive Design */
@media (max-width: 768px) {
.app-actions { .app-actions {
flex-direction: column; flex-direction: column;
} }
.my-packages li { .download-section {
flex-direction: column; max-width: 100%;
align-items: flex-start;
gap: 0.5rem;
} }
} }
.cap-approval-popup {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.cap-approval-content {
background-color: white;
padding: 20px;
border-radius: 8px;
max-width: 80%;
max-height: 80%;
overflow-y: auto;
}
.download-progress {
display: flex;
align-items: center;
gap: 10px;
}
.progress-bar {
width: 100px;
height: 10px;
background-color: #e0e0e0;
border-radius: 5px;
overflow: hidden;
}
.progress-bar-fill {
height: 100%;
background-color: #4caf50;
transition: width 0.3s ease-in-out;
}
.progress-text {
min-width: 50px;
text-align: right;
}
@keyframes spin { @keyframes spin {
0% { 0% {
transform: rotate(0deg); transform: rotate(0deg);

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState, useCallback } from "react"; import React, { useEffect, useState, useCallback, useMemo } from "react";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import { FaDownload, FaCheck, FaTimes, FaPlay, FaSpinner, FaTrash, FaSync } from "react-icons/fa"; import { FaDownload, FaCheck, FaTimes, FaPlay, FaSpinner, FaTrash, FaSync } from "react-icons/fa";
import useAppsStore from "../store"; import useAppsStore from "../store";
@ -8,7 +8,7 @@ import { compareVersions } from "../utils/compareVersions";
export default function AppPage() { export default function AppPage() {
const { id } = useParams(); const { id } = useParams();
const navigate = useNavigate(); const navigate = useNavigate();
const { fetchListing, fetchInstalledApp, uninstallApp, setAutoUpdate } = useAppsStore(); const { fetchListing, fetchInstalledApp, uninstallApp, setAutoUpdate, getLaunchUrl, fetchHomepageApps } = useAppsStore();
const [app, setApp] = useState<AppListing | null>(null); const [app, setApp] = useState<AppListing | null>(null);
const [installedApp, setInstalledApp] = useState<PackageState | null>(null); const [installedApp, setInstalledApp] = useState<PackageState | null>(null);
const [currentVersion, setCurrentVersion] = useState<string | null>(null); const [currentVersion, setCurrentVersion] = useState<string | null>(null);
@ -57,6 +57,21 @@ export default function AppPage() {
} }
}, [id, fetchListing, fetchInstalledApp]); }, [id, fetchListing, fetchInstalledApp]);
const handleLaunch = useCallback(() => {
if (app) {
const launchUrl = getLaunchUrl(`${app.package_id.package_name}:${app.package_id.publisher_node}`);
if (launchUrl) {
window.location.href = launchUrl;
}
}
}, [app, getLaunchUrl]);
const canLaunch = useMemo(() => {
if (!app) return false;
return !!getLaunchUrl(`${app.package_id.package_name}:${app.package_id.publisher_node}`);
}, [app, getLaunchUrl]);
const handleUninstall = async () => { const handleUninstall = async () => {
if (!app) return; if (!app) return;
setIsUninstalling(true); setIsUninstalling(true);
@ -88,16 +103,13 @@ export default function AppPage() {
useEffect(() => { useEffect(() => {
loadData(); loadData();
}, [loadData]); fetchHomepageApps();
}, [loadData, fetchHomepageApps]);
const handleDownload = () => { const handleDownload = () => {
navigate(`/download/${id}`); navigate(`/download/${id}`);
}; };
const handleLaunch = () => {
window.location.href = `/${app?.package_id.package_name}:${app?.package_id.package_name}:${app?.package_id.publisher_node}/`;
};
if (isLoading) { if (isLoading) {
return <div className="app-page"><h4>Loading app details...</h4></div>; return <div className="app-page"><h4>Loading app details...</h4></div>;
} }
@ -150,8 +162,12 @@ export default function AppPage() {
<div className="app-actions"> <div className="app-actions">
{installedApp && ( {installedApp && (
<> <>
<button onClick={handleLaunch} className="primary"> <button
<FaPlay /> Launch onClick={handleLaunch}
className="primary"
disabled={!canLaunch}
>
<FaPlay /> {canLaunch ? 'Launch' : 'No UI found for app'}
</button> </button>
<button onClick={handleUninstall} className="secondary" disabled={isUninstalling}> <button onClick={handleUninstall} className="secondary" disabled={isUninstalling}>
{isUninstalling ? <FaSpinner className="fa-spin" /> : <FaTrash />} Uninstall {isUninstalling ? <FaSpinner className="fa-spin" /> : <FaTrash />} Uninstall

View File

@ -17,6 +17,8 @@ export default function DownloadPage() {
installApp, installApp,
removeDownload, removeDownload,
clearAllActiveDownloads, clearAllActiveDownloads,
fetchHomepageApps,
getLaunchUrl
} = useAppsStore(); } = useAppsStore();
const [showMetadata, setShowMetadata] = useState(false); const [showMetadata, setShowMetadata] = useState(false);
@ -35,8 +37,9 @@ export default function DownloadPage() {
if (id) { if (id) {
fetchData(id); fetchData(id);
clearAllActiveDownloads(); clearAllActiveDownloads();
fetchHomepageApps();
} }
}, [id, fetchData, clearAllActiveDownloads]); }, [id, fetchData, clearAllActiveDownloads, fetchHomepageApps]);
const handleMirrorSelect = useCallback((mirror: string, status: boolean | null | 'http') => { const handleMirrorSelect = useCallback((mirror: string, status: boolean | null | 'http') => {
setSelectedMirror(mirror); setSelectedMirror(mirror);
@ -145,9 +148,17 @@ export default function DownloadPage() {
const handleLaunch = useCallback(() => { const handleLaunch = useCallback(() => {
if (app) { if (app) {
navigate(`/${app.package_id.package_name}:${app.package_id.package_name}:${app.package_id.publisher_node}/`); const launchUrl = getLaunchUrl(`${app.package_id.package_name}:${app.package_id.publisher_node}`);
if (launchUrl) {
window.location.href = launchUrl;
}
} }
}, [app, navigate]); }, [app, getLaunchUrl]);
const canLaunch = useMemo(() => {
if (!app) return false;
return !!getLaunchUrl(`${app.package_id.package_name}:${app.package_id.publisher_node}`);
}, [app, getLaunchUrl]);
if (!app) { if (!app) {
return <div className="downloads-page"><h4>Loading app details...</h4></div>; return <div className="downloads-page"><h4>Loading app details...</h4></div>;
@ -156,36 +167,48 @@ export default function DownloadPage() {
return ( return (
<div className="downloads-page"> <div className="downloads-page">
<div className="app-header"> <div className="app-header">
<h2>{app.metadata?.name || app.package_id.package_name}</h2> <div className="app-title-container">
{app.metadata?.image && (
<img src={app.metadata.image} alt={app.metadata?.name || app.package_id.package_name} className="app-icon" />
)}
<div className="app-title">
<h2>{app.metadata?.name || app.package_id.package_name}</h2>
<p className="app-id">{`${app.package_id.package_name}.${app.package_id.publisher_node}`}</p>
</div>
</div>
{installedApp && ( {installedApp && (
<button onClick={handleLaunch} className="launch-button"> <button
<FaPlay /> Launch onClick={handleLaunch}
className="launch-button"
disabled={!canLaunch}
>
<FaPlay /> {canLaunch ? 'Launch' : 'No UI found for app'}
</button> </button>
)} )}
</div> </div>
<p>{app.metadata?.description}</p> <p className="app-description">{app.metadata?.description}</p>
<div className="version-selector"> <div className="download-section">
<select <select
value={selectedVersion} value={selectedVersion}
onChange={(e) => setSelectedVersion(e.target.value)} onChange={(e) => setSelectedVersion(e.target.value)}
className="version-selector"
> >
{sortedVersions.map(({ version }, index) => ( <option value="">Select version</option>
<option key={version} value={version}> {sortedVersions.map((version) => (
{version} {index === 0 ? "(newest)" : ""} <option key={version.version} value={version.version}>
{installedApp && installedApp.our_version_hash === sortedVersions[index].hash ? " (installed)" : ""} {version.version}
</option> </option>
))} ))}
</select> </select>
</div>
<div className="download-section">
<MirrorSelector <MirrorSelector
packageId={id} packageId={id}
onMirrorSelect={handleMirrorSelect} onMirrorSelect={handleMirrorSelect}
/> />
{isCurrentVersionInstalled ? ( {isCurrentVersionInstalled ? (
<button className="installed-button" disabled> <button className="action-button installed-button" disabled>
<FaRocket /> Installed <FaRocket /> Installed
</button> </button>
) : isDownloaded ? ( ) : isDownloaded ? (
@ -196,7 +219,7 @@ export default function DownloadPage() {
handleInstall(versionData.version, versionData.hash); handleInstall(versionData.version, versionData.hash);
} }
}} }}
className="install-button" className="action-button install-button"
> >
<FaRocket /> Install <FaRocket /> Install
</button> </button>
@ -204,12 +227,11 @@ export default function DownloadPage() {
<button <button
onClick={handleDownload} onClick={handleDownload}
disabled={!canDownload} disabled={!canDownload}
className="download-button" className="action-button download-button"
> >
{isDownloading ? ( {isDownloading ? (
<> <>
<FaSpinner className="fa-spin" /> <FaSpinner className="fa-spin" /> Downloading... {downloadProgress}%
Downloading... {downloadProgress}%
</> </>
) : ( ) : (
<> <>

View File

@ -1,6 +1,6 @@
import { create } from 'zustand' import { create } from 'zustand'
import { persist } from 'zustand/middleware' import { persist } from 'zustand/middleware'
import { PackageState, AppListing, MirrorCheckFile, PackageManifest, DownloadItem } from '../types/Apps' import { PackageState, AppListing, MirrorCheckFile, PackageManifest, DownloadItem, HomepageApp } from '../types/Apps'
import { HTTP_STATUS } from '../constants/http' import { HTTP_STATUS } from '../constants/http'
import KinodeClientApi from "@kinode/client-api" import KinodeClientApi from "@kinode/client-api"
import { WEBSOCKET_URL } from '../utils/ws' import { WEBSOCKET_URL } from '../utils/ws'
@ -13,6 +13,7 @@ interface AppsStore {
downloads: Record<string, DownloadItem[]> downloads: Record<string, DownloadItem[]>
ourApps: AppListing[] ourApps: AppListing[]
ws: KinodeClientApi ws: KinodeClientApi
homepageApps: HomepageApp[]
activeDownloads: Record<string, { downloaded: number, total: number }> activeDownloads: Record<string, { downloaded: number, total: number }>
fetchData: (id: string) => Promise<void> fetchData: (id: string) => Promise<void>
@ -25,6 +26,9 @@ interface AppsStore {
fetchDownloadsForApp: (id: string) => Promise<DownloadItem[]> fetchDownloadsForApp: (id: string) => Promise<DownloadItem[]>
checkMirror: (node: string) => Promise<MirrorCheckFile | null> checkMirror: (node: string) => Promise<MirrorCheckFile | null>
fetchHomepageApps: () => Promise<void>
getLaunchUrl: (id: string) => string | null
installApp: (id: string, version_hash: string) => Promise<void> installApp: (id: string, version_hash: string) => Promise<void>
uninstallApp: (id: string) => Promise<void> uninstallApp: (id: string) => Promise<void>
downloadApp: (id: string, version_hash: string, downloadFrom: string) => Promise<void> downloadApp: (id: string, version_hash: string, downloadFrom: string) => Promise<void>
@ -49,6 +53,8 @@ const useAppsStore = create<AppsStore>()(
downloads: {}, downloads: {},
ourApps: [], ourApps: [],
activeDownloads: {}, activeDownloads: {},
homepageApps: [],
fetchData: async (id: string) => { fetchData: async (id: string) => {
if (!id) return; if (!id) return;
@ -174,6 +180,26 @@ const useAppsStore = create<AppsStore>()(
return []; return [];
}, },
fetchHomepageApps: async () => {
try {
const res = await fetch('/apps');
if (res.status === HTTP_STATUS.OK) {
const data: HomepageApp[] = await res.json();
set({ homepageApps: data });
}
} catch (error) {
console.error("Error fetching homepage apps:", error);
}
},
getLaunchUrl: (id: string) => {
const app = get().homepageApps.find(app => `${app.package}:${app.publisher}` === id);
if (app && app.path) {
return app.path;
}
return null;
},
checkMirror: async (node: string) => { checkMirror: async (node: string) => {
try { try {
const res = await fetch(`${BASE_URL}/mirrorcheck/${node}`); const res = await fetch(`${BASE_URL}/mirrorcheck/${node}`);

View File

@ -70,3 +70,16 @@ export interface PackageManifest {
grant_capabilities: any[] grant_capabilities: any[]
public: boolean public: boolean
} }
export interface HomepageApp {
id: string;
process: string;
package: string;
publisher: string;
path?: string;
label: string;
base64_icon?: string;
widget?: string;
order: number;
favorite: boolean;
}