feat: persistent app install state across navigation (#9)
Move installingApps from local refs in Marketplace/Discover to the global server store. Install progress now persists when navigating between views. My Apps shows installing overlay with progress bar for apps being installed from the Marketplace. Changes: - server.ts: add installingApps Map + helpers to store - Marketplace.vue: use store's installingApps instead of local ref - Discover.vue: same - Apps.vue: pass isInstalling + installProgress to AppCard - AppCard.vue: add amber installing overlay with progress bar 522 tests pass, vue-tsc clean. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
840ecfaa5f
commit
2e29a41627
@@ -112,6 +112,7 @@ import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'
|
||||
import { useRouter, useRoute, RouterLink } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { useServerStore } from '@/stores/server'
|
||||
import { rpcClient } from '@/api/rpc-client'
|
||||
import { useMarketplaceApp } from '@/composables/useMarketplaceApp'
|
||||
import { useAppLauncherStore } from '@/stores/appLauncher'
|
||||
@@ -119,7 +120,6 @@ import MarketplaceAppCard from './marketplace/MarketplaceAppCard.vue'
|
||||
import MarketplaceFilterModal from './marketplace/MarketplaceFilterModal.vue'
|
||||
import {
|
||||
type MarketplaceApp,
|
||||
type InstallProgress,
|
||||
INSTALLED_ALIASES,
|
||||
getAppTier,
|
||||
categorizeCommunityApp,
|
||||
@@ -129,6 +129,7 @@ import {
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const store = useAppStore()
|
||||
const server = useServerStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
const showStagger = !marketplaceAnimationDone
|
||||
@@ -152,8 +153,8 @@ const categories = computed(() => [
|
||||
{ id: 'other', name: t('marketplace.other') }
|
||||
])
|
||||
|
||||
// Installation state - support multiple concurrent installations
|
||||
const installingApps = ref<Map<string, InstallProgress>>(new Map())
|
||||
// Installation state — uses global store so it persists across navigation
|
||||
const installingApps = server.installingApps
|
||||
const maxAttempts = ref(60)
|
||||
|
||||
// Watch WebSocket data for real install progress from backend
|
||||
@@ -162,8 +163,8 @@ watch(() => store.packages, (packages) => {
|
||||
for (const [appId, pkg] of Object.entries(packages)) {
|
||||
if ((pkg.state as string) === 'installing') {
|
||||
const progress = pkg['install-progress']
|
||||
if (!installingApps.value.has(appId)) {
|
||||
installingApps.value.set(appId, {
|
||||
if (!installingApps.has(appId)) {
|
||||
installingApps.set(appId, {
|
||||
id: appId,
|
||||
title: pkg.manifest?.title || appId,
|
||||
status: 'downloading',
|
||||
@@ -173,19 +174,19 @@ watch(() => store.packages, (packages) => {
|
||||
})
|
||||
}
|
||||
if (progress) {
|
||||
const current = installingApps.value.get(appId)!
|
||||
const current = installingApps.get(appId)!
|
||||
const pct = progress.size > 0 ? Math.round((progress.downloaded / progress.size) * 100) : 0
|
||||
const downloadedMB = (progress.downloaded / (1024 * 1024)).toFixed(1)
|
||||
const totalMB = (progress.size / (1024 * 1024)).toFixed(1)
|
||||
installingApps.value.set(appId, {
|
||||
installingApps.set(appId, {
|
||||
...current,
|
||||
status: 'downloading',
|
||||
progress: Math.min(pct, 95),
|
||||
message: progress.size > 0 ? `Downloading: ${downloadedMB} / ${totalMB} MB (${pct}%)` : t('marketplace.downloading'),
|
||||
})
|
||||
}
|
||||
} else if (installingApps.value.has(appId) && (pkg.state as string) !== 'installing') {
|
||||
installingApps.value.delete(appId)
|
||||
} else if (installingApps.has(appId) && (pkg.state as string) !== 'installing') {
|
||||
installingApps.delete(appId)
|
||||
}
|
||||
}
|
||||
}, { deep: true })
|
||||
@@ -402,11 +403,11 @@ onBeforeUnmount(() => {
|
||||
|
||||
function startInstallPolling(appId: string, statusMessage: string) {
|
||||
const interval = trackInterval(() => {
|
||||
const current = installingApps.value.get(appId)
|
||||
const current = installingApps.get(appId)
|
||||
if (!current) { clearTrackedInterval(interval); return }
|
||||
|
||||
const newAttempt = current.attempt + 1
|
||||
installingApps.value.set(appId, {
|
||||
installingApps.set(appId, {
|
||||
...current,
|
||||
attempt: newAttempt,
|
||||
progress: Math.min(60 + (newAttempt * 0.5), 95),
|
||||
@@ -415,49 +416,49 @@ function startInstallPolling(appId: string, statusMessage: string) {
|
||||
|
||||
if (isInstalled(appId)) {
|
||||
clearTrackedInterval(interval)
|
||||
installingApps.value.set(appId, { ...current, status: 'complete', progress: 100, message: 'Installation complete!' })
|
||||
trackTimeout(() => { installingApps.value.delete(appId) }, 2000)
|
||||
installingApps.set(appId, { ...current, status: 'complete', progress: 100, message: 'Installation complete!' })
|
||||
trackTimeout(() => { installingApps.delete(appId) }, 2000)
|
||||
} else if (newAttempt >= maxAttempts.value) {
|
||||
clearTrackedInterval(interval)
|
||||
installingApps.value.set(appId, { ...current, status: 'error', progress: 0, message: 'Installation timeout' })
|
||||
trackTimeout(() => { installingApps.value.delete(appId) }, 5000)
|
||||
installingApps.set(appId, { ...current, status: 'error', progress: 0, message: 'Installation timeout' })
|
||||
trackTimeout(() => { installingApps.delete(appId) }, 5000)
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
async function installApp(app: MarketplaceApp) {
|
||||
if (installingApps.value.has(app.id) || isInstalled(app.id)) return
|
||||
if (installingApps.has(app.id) || isInstalled(app.id)) return
|
||||
|
||||
installingApps.value.set(app.id, {
|
||||
installingApps.set(app.id, {
|
||||
id: app.id, title: app.title ?? app.id, status: 'downloading', progress: 10, message: 'Preparing installation...', attempt: 0
|
||||
})
|
||||
|
||||
try {
|
||||
const installUrl = app.url || app.manifestUrl || app.s9pkUrl
|
||||
|
||||
installingApps.value.set(app.id, { ...installingApps.value.get(app.id)!, status: 'downloading', progress: 30, message: 'Downloading package...' })
|
||||
installingApps.set(app.id, { ...installingApps.get(app.id)!, status: 'downloading', progress: 30, message: 'Downloading package...' })
|
||||
|
||||
await rpcClient.call({ method: 'package.install', params: { id: app.id, url: installUrl, version: app.version } })
|
||||
|
||||
installingApps.value.set(app.id, { ...installingApps.value.get(app.id)!, status: 'installing', progress: 60, message: 'Installing package...' })
|
||||
installingApps.set(app.id, { ...installingApps.get(app.id)!, status: 'installing', progress: 60, message: 'Installing package...' })
|
||||
|
||||
startInstallPolling(app.id, 'Starting application...')
|
||||
} catch (err) {
|
||||
if (import.meta.env.DEV) console.error('Installation failed:', err)
|
||||
installingApps.value.set(app.id, { ...installingApps.value.get(app.id)!, status: 'error', progress: 0, message: `Failed: ${err}` })
|
||||
trackTimeout(() => { installingApps.value.delete(app.id) }, 5000)
|
||||
installingApps.set(app.id, { ...installingApps.get(app.id)!, status: 'error', progress: 0, message: `Failed: ${err}` })
|
||||
trackTimeout(() => { installingApps.delete(app.id) }, 5000)
|
||||
}
|
||||
}
|
||||
|
||||
async function installCommunityApp(app: MarketplaceApp) {
|
||||
if (installingApps.value.has(app.id) || isInstalled(app.id) || !app.dockerImage) return
|
||||
if (installingApps.has(app.id) || isInstalled(app.id) || !app.dockerImage) return
|
||||
|
||||
installingApps.value.set(app.id, {
|
||||
installingApps.set(app.id, {
|
||||
id: app.id, title: app.title ?? app.id, status: 'downloading', progress: 10, message: 'Pulling Docker image...', attempt: 0
|
||||
})
|
||||
|
||||
try {
|
||||
installingApps.value.set(app.id, { ...installingApps.value.get(app.id)!, status: 'downloading', progress: 20, message: 'Downloading container image...' })
|
||||
installingApps.set(app.id, { ...installingApps.get(app.id)!, status: 'downloading', progress: 20, message: 'Downloading container image...' })
|
||||
|
||||
await rpcClient.call({
|
||||
method: 'package.install',
|
||||
@@ -465,13 +466,13 @@ async function installCommunityApp(app: MarketplaceApp) {
|
||||
timeout: 180000
|
||||
})
|
||||
|
||||
installingApps.value.set(app.id, { ...installingApps.value.get(app.id)!, status: 'installing', progress: 60, message: 'Starting container...' })
|
||||
installingApps.set(app.id, { ...installingApps.get(app.id)!, status: 'installing', progress: 60, message: 'Starting container...' })
|
||||
|
||||
startInstallPolling(app.id, 'Initializing application...')
|
||||
} catch (err) {
|
||||
if (import.meta.env.DEV) console.error('[Marketplace] Installation failed:', err)
|
||||
installingApps.value.set(app.id, { ...installingApps.value.get(app.id)!, status: 'error', progress: 0, message: `Failed: ${err}` })
|
||||
trackTimeout(() => { installingApps.value.delete(app.id) }, 5000)
|
||||
installingApps.set(app.id, { ...installingApps.get(app.id)!, status: 'error', progress: 0, message: `Failed: ${err}` })
|
||||
trackTimeout(() => { installingApps.delete(app.id) }, 5000)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user