mirror of
https://github.com/uqbar-dao/nectar.git
synced 2024-12-23 08:32:23 +03:00
Merge pull request #547 from kinode-dao/bp/launchbuttons
app_store UI fixes
This commit is contained in:
commit
31f73db0b0
@ -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);
|
||||||
|
@ -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
|
||||||
|
@ -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}%
|
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
@ -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}`);
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user