diff --git a/kinode/packages/app_store/app_store/src/http_api.rs b/kinode/packages/app_store/app_store/src/http_api.rs index 2e7e87b0..f60d41f2 100644 --- a/kinode/packages/app_store/app_store/src/http_api.rs +++ b/kinode/packages/app_store/app_store/src/http_api.rs @@ -566,7 +566,7 @@ fn serve_paths( )); }; - let downloads = Address::from_str("downloads@downloads:app_store:sys")?; + let downloads = Address::from_str("our@downloads:app_store:sys")?; match method { // start mirroring an app diff --git a/kinode/packages/app_store/ui/src/App.tsx b/kinode/packages/app_store/ui/src/App.tsx index b30fa74c..aa112f28 100644 --- a/kinode/packages/app_store/ui/src/App.tsx +++ b/kinode/packages/app_store/ui/src/App.tsx @@ -8,7 +8,6 @@ import StorePage from "./pages/StorePage"; import AppPage from "./pages/AppPage"; import DownloadPage from "./pages/DownloadPage"; import PublishPage from "./pages/PublishPage"; -import Testing from "./pages/Testing"; import MyDownloadsPage from "./pages/MyDownloadsPage"; @@ -21,9 +20,7 @@ function App() {
- - } /> } /> } /> } /> diff --git a/kinode/packages/app_store/ui/src/index.css b/kinode/packages/app_store/ui/src/index.css index 79afa577..e1979d03 100644 --- a/kinode/packages/app_store/ui/src/index.css +++ b/kinode/packages/app_store/ui/src/index.css @@ -1,23 +1,57 @@ -/* App-specific styles */ +/* Base styles */ +body { + font-family: var(--font-family-main); + line-height: 1.6; + color: light-dark(var(--off-black), var(--off-white)); + background-color: light-dark(var(--tan), var(--tasteful-dark)); +} + +/* Layout */ +.app-content { + max-width: 1000px; + margin: 0 auto; + padding: 1rem; +} + +/* Typography */ +h1, +h2, +h3, +h4, +h5, +h6 { + color: var(--orange); + margin-bottom: 1rem; +} + +a { + color: var(--blue); + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +/* Header */ .app-header { background-color: light-dark(var(--off-white), var(--off-black)); - padding: 1rem 2rem; + padding: 1rem; + margin-bottom: 1rem; display: flex; justify-content: space-between; align-items: center; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } .header-left { display: flex; align-items: center; + gap: 1rem; } .header-left h1 { - font-size: 1.5rem; margin: 0; - margin-right: 2rem; - color: var(--orange); + font-size: 1.5rem; } .header-left nav { @@ -26,487 +60,24 @@ } .header-left nav a { - color: light-dark(var(--off-black), var(--off-white)); + color: var(--orange); text-decoration: none; - padding: 0.5rem 1rem; - border-radius: 4px; - transition: background-color 0.3s ease; + padding: 0.5rem; + border-radius: var(--border-radius); } .header-left nav a:hover, .header-left nav a.active { - background-color: light-dark(var(--orange), var(--dark-orange)); - color: var(--white); -} - -.header-right { - display: flex; - align-items: center; -} - -.app-content { - 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: 10px; - min-width: 200px; -} - -.app-actions-buttons { - display: flex; - flex-wrap: wrap; - gap: 10px; -} - -.app-actions-buttons button { - min-width: 120px; -} - -.mirror-selection { - width: 100%; - margin-bottom: 10px; -} - -.mirror-selection select { - width: 100%; - padding: 8px; - border-radius: 4px; - border: 1px solid var(--gray); - background-color: light-dark(var(--off-white), var(--off-black)); - color: light-dark(var(--off-black), var(--off-white)); -} - -.external-link { - display: inline-block; - margin-top: 10px; -} - -@media (max-width: 768px) { - .app-content { - flex-direction: column; - } - - .app-info-column, - .app-actions-column { - flex: 1; - } -} - -.special-appstore-background { - background-image: linear-gradient(45deg, var(--tan) 25%, transparent 25%), - linear-gradient(-45deg, var(--tan) 25%, transparent 25%), - linear-gradient(45deg, transparent 75%, var(--tan) 75%), - linear-gradient(-45deg, transparent 75%, var(--tan) 75%); - background-size: 20px 20px; - 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 { - 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; -} - -.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 Page Styles */ -.app-page { - max-width: 1000px; - margin: 0 auto; - padding: 20px; -} - -.app-header { - display: flex; - align-items: center; - margin-bottom: 20px; -} - -.app-icon { - width: 100px; - height: 100px; - margin-right: 20px; - border-radius: 12px; - object-fit: cover; -} - -.app-title { - flex: 1; -} - -.app-title h2 { - margin: 0; - color: var(--orange); -} - -.app-id { - font-family: monospace; - color: light-dark(var(--gray), var(--off-white)); - margin-top: 5px; -} - -.app-description { - margin-bottom: 20px; - color: light-dark(var(--gray), var(--off-white)); - line-height: 1.5; -} - -.app-details { - display: flex; - justify-content: space-between; - gap: 2rem; -} - -.app-info { - flex: 1; -} - -.info-section { - margin-bottom: 20px; -} - -.info-section h3 { - color: var(--orange); - margin-bottom: 10px; - cursor: pointer; - display: flex; - align-items: center; - justify-content: space-between; - padding: 10px; - background-color: light-dark(var(--tan), var(--maroon)); - border-radius: 4px; -} - -.detail-list li { - display: flex; - justify-content: space-between; - align-items: center; - padding: 10px 0; - border-bottom: 1px solid light-dark(var(--gray-light), var(--gray)); -} - -.detail-list li:last-child { - border-bottom: none; -} - -.status-icon { - display: flex; - align-items: center; - justify-content: center; - width: 20px; - height: 20px; - border-radius: 50%; - background-color: light-dark(var(--gray-light), var(--gray)); -} - -.installed, -.verified, -.approved, -.mirroring, -.auto-update { - color: var(--orange); -} - -.not-installed, -.not-verified, -.not-approved, -.not-mirroring, -.no-auto-update { - color: var(--ansi-red); -} - -.hash { - font-family: monospace; - font-size: 0.9em; - word-break: break-all; - color: light-dark(var(--gray), var(--off-white)); -} - -.toggle-button { - display: flex; - align-items: center; - justify-content: center; - padding: 5px 10px; - font-size: 0.9em; - background-color: light-dark(var(--gray-light), var(--gray)); - color: light-dark(var(--off-black), var(--off-white)); - border: none; - border-radius: 20px; - cursor: pointer; - transition: all 0.3s ease; -} - -.toggle-button.active { - background-color: var(--orange); - color: var(--white); -} - -.toggle-button svg { - margin-right: 5px; -} - -.app-actions { - display: flex; - flex-direction: column; - gap: 10px; - min-width: 150px; -} - -.screenshot-container { - display: flex; - flex-wrap: wrap; - gap: 10px; - margin-top: 10px; -} - -.app-screenshot { - max-width: 200px; - max-height: 200px; - border-radius: 8px; - object-fit: cover; -} - -/* My Apps Page Styles */ -.my-apps-page { - padding: 2rem; -} - -.my-apps-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 1rem; -} - -.publish-button { - display: flex; - align-items: center; - background-color: var(--orange); - color: var(--white); - border: none; - padding: 0.5rem 1rem; - border-radius: 4px; - cursor: pointer; - transition: background-color 0.3s ease; -} - -.publish-button:hover { - background-color: var(--dark-orange); -} - -.search-input { - width: 100%; - padding: 0.5rem; - font-size: 1rem; - border: 1px solid var(--gray); - border-radius: 4px; - margin-bottom: 1rem; -} - -.apps-list { - display: flex; - flex-direction: column; - gap: 2rem; -} - -.app-category h2 { - margin-bottom: 1rem; - color: var(--orange); -} - -.app-entry { - display: block; - background-color: light-dark(var(--white), var(--off-black)); - border: 1px solid var(--gray); - border-radius: 8px; - padding: 1rem; - margin-bottom: 1rem; - transition: transform 0.3s ease, box-shadow 0.3s ease; -} - -.app-entry:hover { - transform: translateY(-3px); - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); -} - -.app-entry h3 { - margin-bottom: 0.5rem; - color: var(--blue); -} - -.app-entry p { - font-size: 0.9rem; - color: light-dark(var(--gray), var(--off-white)); -} - -/* Publish Page Styles */ -.publish-page { - padding: 2rem; - max-width: 600px; - margin: 0 auto; -} - -.publisher-info { - background-color: light-dark(var(--tan), var(--maroon)); - padding: 0.5rem 1rem; - border-radius: 4px; - margin-bottom: 1rem; -} - -.publisher-info .address { - font-family: monospace; - font-weight: bold; -} - -.publish-form { +/* Forms */ +form { display: flex; flex-direction: column; gap: 1rem; + max-width: 500px; } .form-group { @@ -514,34 +85,67 @@ button svg, flex-direction: column; } -.form-group label { +label { margin-bottom: 0.5rem; } -.form-group input[type="text"] { +input, +select { padding: 0.5rem; border: 1px solid var(--gray); - border-radius: 4px; + border-radius: var(--border-radius); } -.form-group input[type="checkbox"] { - margin-right: 0.5rem; +/* Buttons */ +button { + padding: 0.5rem 1rem; + background-color: var(--orange); + color: var(--white); + border: none; + border-radius: var(--border-radius); + cursor: pointer; } -.help-text { - font-size: 0.9rem; - color: var(--gray); - margin-top: 0.25rem; +button:hover { + background-color: var(--dark-orange); } -.message { - padding: 1rem; - border-radius: 4px; +button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Tables */ +table { + width: 100%; + border-collapse: collapse; margin-bottom: 1rem; } -.message.info { - background-color: var(--blue); +th, +td { + padding: 0.5rem; + text-align: left; + border-bottom: 1px solid var(--gray); +} + +/* App Icon */ +.app-icon { + width: 64px; + height: 64px; + object-fit: cover; + border-radius: var(--border-radius); +} + +/* Messages */ +.message { + padding: 1rem; + border-radius: var(--border-radius); + margin-bottom: 1rem; +} + +.message.error { + background-color: var(--red); color: var(--white); } @@ -550,11 +154,150 @@ button svg, color: var(--white); } -.message.error { - background-color: var(--ansi-red); +.message.info { + background-color: var(--blue); color: var(--white); } +/* Publisher Info */ +.publisher-info { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 1rem; +} + +.address { + font-family: monospace; + background-color: var(--gray); + padding: 0.25rem 0.5rem; + border-radius: var(--border-radius); +} + +/* Help Text */ +.help-text { + font-size: 0.9rem; + color: var(--gray); + margin-top: 0.25rem; +} + +/* Status Icons */ +.status-icon { + display: inline-flex; + align-items: center; +} + +.installed { + color: var(--green); +} + +.not-installed { + color: var(--red); +} + +/* App Title */ +.app-title { + display: flex; + flex-direction: column; +} + +.app-id { + font-size: 0.9rem; + color: var(--gray); +} + +/* Detail List */ +.detail-list { + list-style-type: none; + padding: 0; +} + +.detail-list li { + display: flex; + justify-content: space-between; + margin-bottom: 0.5rem; +} + +/* Error Message */ +.error-message { + display: flex; + align-items: center; + gap: 0.5rem; + color: var(--red); + margin-bottom: 1rem; +} + +/* 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; } @@ -572,376 +315,55 @@ button svg, border-bottom: 1px solid var(--gray); } -.my-packages button { - background-color: var(--ansi-red); - color: var(--white); - border: none; - padding: 0.25rem 0.5rem; - border-radius: 4px; - cursor: pointer; -} - -.my-packages button:hover { - background-color: #c62828; -} - -/* Mirrors Dropdown Styles */ -.mirrors-dropdown { - position: relative; -} - -.mirrors-dropdown-toggle { - background: none; - border: none; - cursor: pointer; - display: flex; - align-items: center; - color: light-dark(var(--off-black), var(--off-white)); -} - -.mirrors-list { - margin-top: 10px; -} - -.mirror-item { - display: flex; - align-items: center; - margin-bottom: 8px; -} - -.mirror-address { - flex-grow: 1; - margin-right: 10px; - font-size: 0.9em; -} - -.check-button { - background: none; - border: none; - cursor: pointer; - padding: 5px; - margin-right: 10px; - display: inline-flex; - align-items: center; - justify-content: center; - width: 24px; - height: 24px; -} - -.check-button svg { - width: 16px; - height: 16px; - color: light-dark(var(--off-black), var(--off-white)); -} - -.spinning { - animation: spin 1s linear infinite; -} - -@keyframes spin { - 0% { - transform: rotate(0deg); - } - - 100% { - transform: rotate(360deg); - } -} - -.mirror-status { - display: flex; - align-items: center; -} - -.online { - color: var(--green); -} - -.offline { - color: var(--ansi-red); -} - -.error-message { - margin-left: 5px; - font-size: 0.8em; - 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; -} - - -.hash { - font-family: monospace; - word-break: break-all; -} - -.expandable-item { - background-color: light-dark(var(--tan), var(--maroon)); - border-radius: 4px; - overflow: hidden; -} - -.item-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 10px; -} - -.dropdown-toggle { - background: none; - border: none; - cursor: pointer; - color: var(--blue); - padding: 5px; -} - -.json-container { - max-height: 300px; - overflow-y: auto; - background-color: light-dark(var(--off-white), var(--off-black)); -} - -.json-display { - font-family: monospace; - font-size: 12px; - white-space: pre-wrap; - word-wrap: break-word; - padding: 10px; - margin: 0; - color: light-dark(var(--off-black), var(--off-white)); -} - -/* Add these new styles */ -.app-actions button { - transition: all 0.3s ease; -} - -.app-actions button:hover:not(:disabled) { - transform: translateY(-2px); - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); -} - -.app-actions button:disabled { - opacity: 0.6; - cursor: not-allowed; -} - -.updating { - background-color: var(--yellow); - color: var(--off-black); -} - -.progress-container { - margin-top: 1rem; -} - -.progress-bar { - height: 10px; - background-color: var(--off-white); - border-radius: 5px; - overflow: hidden; -} - -.progress { - height: 100%; - background-color: var(--blue); - transition: width 0.3s ease; -} - -.progress-percentage { - text-align: center; - font-size: 0.8rem; - margin-top: 0.25rem; -} - -.expandable-item { - background-color: light-dark(var(--tan), var(--maroon)); - border-radius: 4px; - overflow: hidden; - transition: all 0.3s ease; -} - -.expandable-item:hover { - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); -} - -.item-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 10px; - cursor: pointer; -} - -.dropdown-toggle { - background: none; - border: none; - cursor: pointer; - color: var(--blue); - padding: 5px; - transition: transform 0.3s ease; -} - -.dropdown-toggle:hover { - transform: scale(1.1); -} - -.json-container { - max-height: 300px; - overflow-y: auto; - background-color: light-dark(var(--off-white), var(--off-black)); - padding: 10px; - border-top: 1px solid var(--gray); -} - -.json-display { - font-family: monospace; - font-size: 12px; - white-space: pre-wrap; - word-wrap: break-word; - margin: 0; - color: light-dark(var(--off-black), var(--off-white)); -} - +/* Download Page */ .downloads-page { - max-width: 800px; - margin: 0 auto; - padding: 20px; -} - -.app-header { - display: flex; - align-items: center; - margin-bottom: 20px; -} - -.app-icon { - width: 64px; - height: 64px; - margin-right: 20px; -} - -.app-title h2 { - margin: 0; -} - -.app-id { - color: #666; - font-size: 0.9em; -} - -.app-description { - margin-bottom: 20px; + background-color: light-dark(var(--off-white), var(--off-black)); + border-radius: var(--border-radius); + padding: 1rem; } .mirror-selection { - margin-bottom: 20px; + max-width: 300px; + margin-bottom: 1rem; } -.mirror-selection select { - width: 100%; - padding: 10px; - font-size: 1em; -} - -.downloads-table { - width: 100%; - border-collapse: collapse; - margin-bottom: 20px; -} - -.downloads-table th, -.downloads-table td { - padding: 10px; - border: 1px solid #ddd; - text-align: left; -} - -.downloads-table th { - background-color: #f2f2f2; -} - -.download-button, -.install-button { - padding: 5px 10px; - font-size: 0.9em; - cursor: pointer; -} - -.error-message { - color: #d32f2f; - margin-bottom: 20px; +.app-details { + margin-top: 1rem; } .detail-section { - margin-bottom: 20px; -} - -.detail-section h3 { - cursor: pointer; - user-select: none; + background-color: light-dark(var(--tan), var(--tasteful-dark)); + border-radius: var(--border-radius); + padding: 1rem; + margin-bottom: 1rem; } .json-display { - background-color: #f2f2f2; - padding: 10px; - border-radius: 4px; - overflow-x: auto; - font-size: 0.9em; + white-space: pre-wrap; + word-break: break-all; + max-height: 300px; + overflow-y: auto; + background-color: light-dark(var(--off-white), var(--off-black)); + padding: 0.5rem; + border-radius: var(--border-radius); +} + +/* My Downloads Page */ +.my-downloads-page { + background-color: light-dark(var(--off-white), var(--off-black)); + border-radius: var(--border-radius); + padding: 1rem; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .app-actions { + flex-direction: column; + } + + .my-packages li { + flex-direction: column; + align-items: flex-start; + gap: 0.5rem; + } } \ No newline at end of file diff --git a/kinode/packages/app_store/ui/src/pages/AppPage.tsx b/kinode/packages/app_store/ui/src/pages/AppPage.tsx index 7ec357b2..363d593e 100644 --- a/kinode/packages/app_store/ui/src/pages/AppPage.tsx +++ b/kinode/packages/app_store/ui/src/pages/AppPage.tsx @@ -3,56 +3,82 @@ import { useNavigate, useParams } from "react-router-dom"; import { FaDownload, FaCheck, FaTimes, FaPlay } from "react-icons/fa"; import useAppsStore from "../store"; import { AppListing, PackageState } from "../types/Apps"; +import { compareVersions } from "../utils/compareVersions"; export default function AppPage() { const { id } = useParams(); const navigate = useNavigate(); - const { listings, installed, fetchListings, fetchInstalled } = useAppsStore(); + const { fetchListing, fetchInstalledApp, installApp } = useAppsStore(); const [app, setApp] = useState(null); const [installedApp, setInstalledApp] = useState(null); const [currentVersion, setCurrentVersion] = useState(null); const [latestVersion, setLatestVersion] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); const loadData = useCallback(async () => { - await Promise.all([fetchListings(), fetchInstalled()]); + if (!id) return; + setIsLoading(true); + setError(null); - const foundApp = listings.find(a => `${a.package_id.package_name}:${a.package_id.publisher_node}` === id) || null; - setApp(foundApp); + try { + const [appData, installedAppData] = await Promise.all([ + fetchListing(id), + fetchInstalledApp(id) + ]); - if (foundApp) { - const foundInstalledApp = installed.find(i => - i.package_id.package_name === foundApp.package_id.package_name && - i.package_id.publisher_node === foundApp.package_id.publisher_node - ) || null; - setInstalledApp(foundInstalledApp); + setApp(appData); + setInstalledApp(installedAppData); - if (foundApp.metadata?.properties?.code_hashes) { - const versions = foundApp.metadata.properties.code_hashes; + if (appData.metadata?.properties?.code_hashes) { + const versions = appData.metadata.properties.code_hashes; if (versions.length > 0) { - setLatestVersion(versions[versions.length - 1][0]); - if (foundInstalledApp) { - const installedVersion = versions.find(([_, hash]) => hash === foundInstalledApp.our_version_hash); + const latestVer = versions.reduce((latest, current) => + compareVersions(current[0], latest[0]) > 0 ? current : latest + )[0]; + setLatestVersion(latestVer); + + if (installedAppData) { + const installedVersion = versions.find(([_, hash]) => hash === installedAppData.our_version_hash); if (installedVersion) { setCurrentVersion(installedVersion[0]); } } } } + } catch (err) { + setError("Failed to load app details. Please try again."); + console.error(err); + } finally { + setIsLoading(false); } - }, [id, fetchListings, fetchInstalled]); + }, [id, fetchListing, fetchInstalledApp]); useEffect(() => { loadData(); }, [loadData]); - if (!app) { - return

App details not found for {id}

; - } - const handleDownload = () => { navigate(`/download/${id}`); }; + const handleLaunch = () => { + // Implement launch functionality + console.log("Launching app:", app?.package_id.package_name); + }; + + if (isLoading) { + return

Loading app details...

; + } + + if (error) { + return

{error}

; + } + + if (!app) { + return

App details not found for {id}

; + } + return (
@@ -89,7 +115,7 @@ export default function AppPage() { Download {installedApp && ( - )} diff --git a/kinode/packages/app_store/ui/src/pages/DownloadPage.tsx b/kinode/packages/app_store/ui/src/pages/DownloadPage.tsx index 23bd5beb..f0fa8b34 100644 --- a/kinode/packages/app_store/ui/src/pages/DownloadPage.tsx +++ b/kinode/packages/app_store/ui/src/pages/DownloadPage.tsx @@ -2,14 +2,14 @@ import React, { useState, useEffect } from "react"; import { useParams } from "react-router-dom"; import { FaDownload, FaCheck, FaSpinner, FaRocket, FaChevronDown, FaChevronUp, FaExclamationTriangle } from "react-icons/fa"; import useAppsStore from "../store"; -import { AppListing, DownloadItem, MirrorCheckFile, PackageManifest } from "../types/Apps"; +import { DownloadItem, PackageManifest, AppListing } from "../types/Apps"; export default function DownloadPage() { const { id } = useParams(); const { listings, fetchListings, fetchDownloadsForApp, downloadApp, installApp, checkMirror, fetchInstalled, installed, getCaps, approveCaps } = useAppsStore(); const [downloads, setDownloads] = useState([]); const [selectedMirror, setSelectedMirror] = useState(null); - const [mirrorStatuses, setMirrorStatuses] = useState<{ [mirror: string]: MirrorCheckFile | null }>({}); + const [mirrorStatuses, setMirrorStatuses] = useState<{ [mirror: string]: { status: 'unchecked' | 'checking' | 'online' | 'offline' } }>({}); const [isDownloading, setIsDownloading] = useState(false); const [isInstalling, setIsInstalling] = useState(false); const [error, setError] = useState(null); @@ -19,8 +19,10 @@ export default function DownloadPage() { const [manifest, setManifest] = useState(null); const [showCapApproval, setShowCapApproval] = useState(false); const [selectedVersion, setSelectedVersion] = useState(null); + const [showManualMirror, setShowManualMirror] = useState(false); + const [manualMirror, setManualMirror] = useState(""); - const app = listings.find(a => `${a.package_id.package_name}:${a.package_id.publisher_node}` === id); + const app = listings[id as string]; useEffect(() => { fetchListings(); @@ -32,20 +34,30 @@ export default function DownloadPage() { useEffect(() => { if (app) { - checkMirrors(); + initializeMirrors(); } }, [app]); - const checkMirrors = async () => { + const initializeMirrors = () => { if (!app) return; const mirrors = [app.package_id.publisher_node, ...(app.metadata?.properties?.mirrors || [])]; - const statuses: { [mirror: string]: MirrorCheckFile | null } = {}; - for (const mirror of mirrors) { + const initialStatuses: { [mirror: string]: { status: 'unchecked' | 'checking' | 'online' | 'offline' } } = {}; + mirrors.forEach(mirror => { + initialStatuses[mirror] = { status: 'unchecked' }; + }); + setMirrorStatuses(initialStatuses); + setSelectedMirror(app.package_id.publisher_node); + mirrors.forEach(checkMirrorStatus); + }; + + const checkMirrorStatus = async (mirror: string) => { + setMirrorStatuses(prev => ({ ...prev, [mirror]: { status: 'checking' } })); + try { const status = await checkMirror(mirror); - statuses[mirror] = status; + setMirrorStatuses(prev => ({ ...prev, [mirror]: { status: status.is_online ? 'online' : 'offline' } })); + } catch (error) { + setMirrorStatuses(prev => ({ ...prev, [mirror]: { status: 'offline' } })); } - setMirrorStatuses(statuses); - setSelectedMirror(statuses[app.package_id.publisher_node]?.is_online ? app.package_id.publisher_node : mirrors.find(m => statuses[m]?.is_online) || null); }; const handleDownload = async (version: string) => { @@ -115,25 +127,64 @@ export default function DownloadPage() {

Select Mirror

+ {(showManualMirror || selectedMirror === manualMirror) && ( +
+ setManualMirror(e.target.value)} + placeholder="Enter mirror node" + /> + + +
+ )}
+

Available Versions

- @@ -147,12 +198,11 @@ export default function DownloadPage() { return false; }); const isDownloaded = !!download; - const isInstalled = installed.some(i => i.package_id.package_name === app.package_id.package_name && i.our_version_hash === version); + const isInstalled = installed[id as string]?.our_version_hash === hash; return ( - @@ -160,7 +210,7 @@ export default function DownloadPage() { {!isDownloaded && ( navigateToItem(item)} className={isFile ? 'file' : 'directory'}>
VersionSize Status Actions
{version}{download?.File?.size ? `${(download.File.size / 1024 / 1024).toFixed(2)} MB` : 'N/A'} {isInstalled ? 'Installed' : isDownloaded ? 'Downloaded' : 'Not downloaded'}
diff --git a/kinode/packages/app_store/ui/src/pages/PublishPage.tsx b/kinode/packages/app_store/ui/src/pages/PublishPage.tsx index cd97c34d..2692c537 100644 --- a/kinode/packages/app_store/ui/src/pages/PublishPage.tsx +++ b/kinode/packages/app_store/ui/src/pages/PublishPage.tsx @@ -8,7 +8,6 @@ import { kinohash } from '../utils/kinohash'; import useAppsStore from "../store"; export default function PublishPage() { - const { state } = useLocation(); const { openConnectModal } = useConnectModal(); const { ourApps, fetchOurApps } = useAppsStore(); const publicClient = usePublicClient(); @@ -83,7 +82,6 @@ export default function PublishPage() { [tba, owner, _data] = data as [string, string, string]; isUpdate = false; // It's a new package, but we might have a publisher TBA currentTBA = (tba && tba !== '0x') ? tba as `0x${string}` : null; - console.log('NEWcurrenttba, isupdate: ', currentTBA, isUpdate) } let metadata = metadataHash; @@ -252,10 +250,10 @@ export default function PublishPage() {

Packages You Own

- {ourApps.length > 0 ? ( + {Object.keys(ourApps).length > 0 ? (
    - {ourApps.map((app) => ( -
  • + {Object.values(ourApps).map((app) => ( +
  • {app.metadata?.name || app.package_id.package_name} diff --git a/kinode/packages/app_store/ui/src/pages/StorePage.tsx b/kinode/packages/app_store/ui/src/pages/StorePage.tsx index 093e45b5..2166024d 100644 --- a/kinode/packages/app_store/ui/src/pages/StorePage.tsx +++ b/kinode/packages/app_store/ui/src/pages/StorePage.tsx @@ -11,12 +11,10 @@ export default function StorePage() { fetchListings(); }, [fetchListings]); - const filteredApps = Array.isArray(listings) - ? listings.filter((app) => - app.package_id.package_name.toLowerCase().includes(searchQuery.toLowerCase()) || - app.metadata?.description?.toLowerCase().includes(searchQuery.toLowerCase()) - ) - : []; + const filteredApps = Object.values(listings).filter((app) => + app.package_id.package_name.toLowerCase().includes(searchQuery.toLowerCase()) || + app.metadata?.description?.toLowerCase().includes(searchQuery.toLowerCase()) + ); return (
    @@ -29,9 +27,9 @@ export default function StorePage() { />
    - {!Array.isArray(listings) ? ( + {Object.keys(listings).length === 0 ? (

    Loading...

    - ) : listings.length === 0 ? ( + ) : filteredApps.length === 0 ? (

    No apps available.

    ) : ( @@ -55,7 +53,6 @@ export default function StorePage() { ); } -// ... rest of the code remains the same interface AppRowProps { app: AppListing; } diff --git a/kinode/packages/app_store/ui/src/pages/Testing.tsx b/kinode/packages/app_store/ui/src/pages/Testing.tsx deleted file mode 100644 index 50026156..00000000 --- a/kinode/packages/app_store/ui/src/pages/Testing.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import React, { useState, useEffect } from 'react' -import useAppsStore from '../store' - -const Testing: React.FC = () => { - const { - fetchListings, - fetchInstalled, - fetchDownloads, - fetchOurApps, - fetchDownloadsForApp, - listings, - installed, - downloads, - ourApps - } = useAppsStore() - const [result, setResult] = useState(null) - const [appId, setAppId] = useState('') - - useEffect(() => { - fetchListings() - fetchInstalled() - fetchDownloads() - fetchOurApps() - }, []) - - const handleAction = async (action: () => Promise, key: string) => { - console.log('in handleAction') - try { - await action() - setResult(JSON.stringify(useAppsStore.getState()[key], null, 2)) - } catch (error) { - setResult(`Error: ${error.message}`) - } - } - - const handleDownloadsForApp = async () => { - try { - const data = await fetchDownloadsForApp(appId) - setResult(JSON.stringify(data, null, 2)) - } catch (error) { - setResult(`Error: ${error.message}`) - } - } - - return ( -
    -

    Testing Page

    -
    - - - {/* */} - -
    -
    - setAppId(e.target.value)} - placeholder="Enter App ID" - /> - -
    -
    {result}
    -
    - ) -} - -export default Testing \ No newline at end of file diff --git a/kinode/packages/app_store/ui/src/store/index.ts b/kinode/packages/app_store/ui/src/store/index.ts index 2e122324..032c4542 100644 --- a/kinode/packages/app_store/ui/src/store/index.ts +++ b/kinode/packages/app_store/ui/src/store/index.ts @@ -8,8 +8,8 @@ import { WEBSOCKET_URL } from '../utils/ws' const BASE_URL = '/main:app_store:sys' interface AppsStore { - listings: AppListing[] - installed: PackageState[] + listings: Record + installed: Record downloads: Record ourApps: AppListing[] ws: KinodeClientApi @@ -18,9 +18,10 @@ interface AppsStore { fetchListings: () => Promise fetchListing: (id: string) => Promise fetchInstalled: () => Promise - fetchDownloads: () => Promise; - fetchOurApps: () => Promise; - fetchDownloadsForApp: (id: string) => Promise; + fetchInstalledApp: (id: string) => Promise + fetchDownloads: () => Promise + fetchOurApps: () => Promise + fetchDownloadsForApp: (id: string) => Promise checkMirror: (node: string) => Promise installApp: (id: string, version_hash: string) => Promise @@ -42,8 +43,8 @@ const appId = (id: string): PackageId => { const useAppsStore = create()( persist( (set, get): AppsStore => ({ - listings: [], - installed: [], + listings: {}, + installed: {}, downloads: {}, ourApps: [], activeDownloads: {}, @@ -77,31 +78,51 @@ const useAppsStore = create()( fetchListings: async () => { const res = await fetch(`${BASE_URL}/apps`) if (res.status === HTTP_STATUS.OK) { - const data = await res.json() - set({ listings: data || [] }) + const data: AppListing[] = await res.json() + const listingsMap = data.reduce((acc, listing) => { + acc[`${listing.package_id.package_name}:${listing.package_id.publisher_node}`] = listing + return acc + }, {} as Record) + set({ listings: listingsMap }) } }, fetchListing: async (id: string) => { const res = await fetch(`${BASE_URL}/apps/${id}`) if (res.status === HTTP_STATUS.OK) { - const listing = await res.json() + const listing: AppListing = await res.json() set((state) => ({ - listings: state.listings.map(l => l.package_id === appId(id) ? listing : l) + listings: { ...state.listings, [id]: listing } })) return listing } - throw new Error(`Failed to get listing for app: ${id}`) + throw new Error(`Failed to fetch listing for app: ${id}`) }, fetchInstalled: async () => { const res = await fetch(`${BASE_URL}/installed`) if (res.status === HTTP_STATUS.OK) { - const installed = await res.json() - set({ installed }) + const data: PackageState[] = await res.json() + const installedMap = data.reduce((acc, pkg) => { + acc[`${pkg.package_id.package_name}:${pkg.package_id.publisher_node}`] = pkg + return acc + }, {} as Record) + set({ installed: installedMap }) } }, + fetchInstalledApp: async (id: string) => { + const res = await fetch(`${BASE_URL}/installed/${id}`) + if (res.status === HTTP_STATUS.OK) { + const installedApp: PackageState = await res.json() + set((state) => ({ + installed: { ...state.installed, [id]: installedApp } + })) + return installedApp + } + return null + }, + fetchDownloads: async () => { const res = await fetch(`${BASE_URL}/downloads`) if (res.status === HTTP_STATUS.OK) { @@ -114,17 +135,16 @@ const useAppsStore = create()( fetchOurApps: async () => { const res = await fetch(`${BASE_URL}/ourapps`) - if (res.status === HTTP_STATUS.OK) { - const data = await res.json() - set({ ourApps: data || [] }) + const data: AppListing[] = await res.json() + set({ ourApps: data }) } }, fetchDownloadsForApp: async (id: string) => { const res = await fetch(`${BASE_URL}/downloads/${id}`) if (res.status === HTTP_STATUS.OK) { - const downloads = await res.json() + const downloads: DownloadItem[] = await res.json() set((state) => ({ downloads: { ...state.downloads, [id]: downloads } })) @@ -136,7 +156,7 @@ const useAppsStore = create()( checkMirror: async (node: string) => { const res = await fetch(`${BASE_URL}/mirrorcheck/${node}`) if (res.status === HTTP_STATUS.OK) { - return await res.json() + return await res.json() as MirrorCheckFile } throw new Error(`Failed to check mirror status for node: ${node}`) }, @@ -173,7 +193,7 @@ const useAppsStore = create()( getCaps: async (id: string) => { const res = await fetch(`${BASE_URL}/apps/${id}/caps`) if (res.status === HTTP_STATUS.OK) { - return await res.json() + return await res.json() as PackageManifest } throw new Error(`Failed to get caps for app: ${id}`) }, diff --git a/kinode/packages/app_store/ui/src/utils/compareVersions.ts b/kinode/packages/app_store/ui/src/utils/compareVersions.ts new file mode 100644 index 00000000..16c954e7 --- /dev/null +++ b/kinode/packages/app_store/ui/src/utils/compareVersions.ts @@ -0,0 +1,12 @@ +// Helper function to compare version strings +export const compareVersions = (v1: string, v2: string) => { + const parts1 = v1.split('.').map(Number); + const parts2 = v2.split('.').map(Number); + for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) { + const part1 = parts1[i] || 0; + const part2 = parts2[i] || 0; + if (part1 > part2) return 1; + if (part1 < part2) return -1; + } + return 0; +}; \ No newline at end of file