Refactor app navigation and enhance background handling in various views
- Added error handling for router navigation to prevent unhandled promise rejections. - Improved background image management in Dashboard.vue to dynamically set images based on route. - Introduced timers for managing loading states and cleanup on component unmount in Apps.vue, Marketplace.vue, and other views. - Updated app detail navigation to ensure smoother transitions and error handling. - Enhanced clipboard copy functionality in Settings.vue with improved user feedback.
This commit is contained in:
@@ -366,7 +366,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { rpcClient } from '@/api/rpc-client'
|
||||
@@ -893,197 +893,110 @@ function viewAppDetails(app: any) {
|
||||
}
|
||||
}
|
||||
|
||||
const activeTimers: ReturnType<typeof setTimeout>[] = []
|
||||
const activeIntervals: ReturnType<typeof setInterval>[] = []
|
||||
|
||||
function trackTimeout(fn: () => void, ms: number) {
|
||||
const id = setTimeout(() => {
|
||||
const idx = activeTimers.indexOf(id)
|
||||
if (idx !== -1) activeTimers.splice(idx, 1)
|
||||
fn()
|
||||
}, ms)
|
||||
activeTimers.push(id)
|
||||
return id
|
||||
}
|
||||
|
||||
function trackInterval(fn: () => void, ms: number) {
|
||||
const id = setInterval(fn, ms)
|
||||
activeIntervals.push(id)
|
||||
return id
|
||||
}
|
||||
|
||||
function clearTrackedInterval(id: ReturnType<typeof setInterval>) {
|
||||
clearInterval(id)
|
||||
const idx = activeIntervals.indexOf(id)
|
||||
if (idx !== -1) activeIntervals.splice(idx, 1)
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
for (const t of activeTimers) clearTimeout(t)
|
||||
activeTimers.length = 0
|
||||
for (const i of activeIntervals) clearInterval(i)
|
||||
activeIntervals.length = 0
|
||||
})
|
||||
|
||||
function startInstallPolling(appId: string, statusMessage: string) {
|
||||
const interval = trackInterval(() => {
|
||||
const current = installingApps.value.get(appId)
|
||||
if (!current) { clearTrackedInterval(interval); return }
|
||||
|
||||
const newAttempt = current.attempt + 1
|
||||
installingApps.value.set(appId, {
|
||||
...current,
|
||||
attempt: newAttempt,
|
||||
progress: Math.min(60 + (newAttempt * 0.5), 95),
|
||||
message: statusMessage
|
||||
})
|
||||
|
||||
if (isInstalled(appId)) {
|
||||
clearTrackedInterval(interval)
|
||||
installingApps.value.set(appId, { ...current, status: 'complete', progress: 100, message: 'Installation complete!' })
|
||||
trackTimeout(() => { installingApps.value.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)
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
async function installApp(app: any) {
|
||||
if (installingApps.value.has(app.id) || isInstalled(app.id)) return
|
||||
|
||||
// Add to installing map
|
||||
installingApps.value.set(app.id, {
|
||||
id: app.id,
|
||||
title: app.title,
|
||||
status: 'downloading',
|
||||
progress: 10,
|
||||
message: 'Preparing installation...',
|
||||
attempt: 0
|
||||
id: app.id, title: app.title, status: 'downloading', progress: 10, message: 'Preparing installation...', attempt: 0
|
||||
})
|
||||
|
||||
try {
|
||||
const installUrl = app.url || app.manifestUrl || app.s9pkUrl
|
||||
console.log('[Marketplace] Installing local app:', { id: app.id, url: installUrl, version: app.version })
|
||||
|
||||
// Update progress
|
||||
installingApps.value.set(app.id, {
|
||||
...installingApps.value.get(app.id)!,
|
||||
status: 'downloading',
|
||||
progress: 30,
|
||||
message: 'Downloading package...'
|
||||
})
|
||||
installingApps.value.set(app.id, { ...installingApps.value.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
|
||||
}
|
||||
})
|
||||
await rpcClient.call({ method: 'package.install', params: { id: app.id, url: installUrl, version: app.version } })
|
||||
|
||||
// Update progress
|
||||
installingApps.value.set(app.id, {
|
||||
...installingApps.value.get(app.id)!,
|
||||
status: 'installing',
|
||||
progress: 60,
|
||||
message: 'Installing package...'
|
||||
})
|
||||
|
||||
// Wait for installation to complete (poll for package to appear)
|
||||
const checkInstalled = setInterval(() => {
|
||||
const current = installingApps.value.get(app.id)
|
||||
if (!current) {
|
||||
clearInterval(checkInstalled)
|
||||
return
|
||||
}
|
||||
|
||||
const newAttempt = current.attempt + 1
|
||||
installingApps.value.set(app.id, {
|
||||
...current,
|
||||
attempt: newAttempt,
|
||||
progress: Math.min(60 + (newAttempt * 0.5), 95),
|
||||
message: 'Starting application...'
|
||||
})
|
||||
|
||||
if (isInstalled(app.id)) {
|
||||
clearInterval(checkInstalled)
|
||||
installingApps.value.set(app.id, {
|
||||
...current,
|
||||
status: 'complete',
|
||||
progress: 100,
|
||||
message: 'Installation complete!'
|
||||
})
|
||||
setTimeout(() => {
|
||||
installingApps.value.delete(app.id)
|
||||
}, 2000)
|
||||
} else if (newAttempt >= maxAttempts.value) {
|
||||
clearInterval(checkInstalled)
|
||||
installingApps.value.set(app.id, {
|
||||
...current,
|
||||
status: 'error',
|
||||
progress: 0,
|
||||
message: 'Installation timeout'
|
||||
})
|
||||
setTimeout(() => {
|
||||
installingApps.value.delete(app.id)
|
||||
}, 5000)
|
||||
}
|
||||
}, 1000)
|
||||
installingApps.value.set(app.id, { ...installingApps.value.get(app.id)!, status: 'installing', progress: 60, message: 'Installing package...' })
|
||||
|
||||
startInstallPolling(app.id, 'Starting application...')
|
||||
} catch (err) {
|
||||
console.error('Installation failed:', err)
|
||||
installingApps.value.set(app.id, {
|
||||
...installingApps.value.get(app.id)!,
|
||||
status: 'error',
|
||||
progress: 0,
|
||||
message: `Failed: ${err}`
|
||||
})
|
||||
setTimeout(() => {
|
||||
installingApps.value.delete(app.id)
|
||||
}, 5000)
|
||||
installingApps.value.set(app.id, { ...installingApps.value.get(app.id)!, status: 'error', progress: 0, message: `Failed: ${err}` })
|
||||
trackTimeout(() => { installingApps.value.delete(app.id) }, 5000)
|
||||
}
|
||||
}
|
||||
|
||||
async function installCommunityApp(app: any) {
|
||||
if (installingApps.value.has(app.id) || isInstalled(app.id) || !app.dockerImage) return
|
||||
|
||||
// Add to installing map
|
||||
installingApps.value.set(app.id, {
|
||||
id: app.id,
|
||||
title: app.title,
|
||||
status: 'downloading',
|
||||
progress: 10,
|
||||
message: 'Pulling Docker image...',
|
||||
attempt: 0
|
||||
id: app.id, title: app.title, status: 'downloading', progress: 10, message: 'Pulling Docker image...', attempt: 0
|
||||
})
|
||||
|
||||
try {
|
||||
console.log(`[Marketplace] Installing Docker app ${app.title} using image ${app.dockerImage}`)
|
||||
|
||||
// Update progress
|
||||
installingApps.value.set(app.id, {
|
||||
...installingApps.value.get(app.id)!,
|
||||
status: 'downloading',
|
||||
progress: 20,
|
||||
message: 'Downloading container image...'
|
||||
})
|
||||
installingApps.value.set(app.id, { ...installingApps.value.get(app.id)!, status: 'downloading', progress: 20, message: 'Downloading container image...' })
|
||||
|
||||
await rpcClient.call({
|
||||
method: 'package.install',
|
||||
params: {
|
||||
id: app.id,
|
||||
dockerImage: app.dockerImage,
|
||||
version: app.version
|
||||
},
|
||||
timeout: 180000 // 3 minutes for large images like Nextcloud
|
||||
params: { id: app.id, dockerImage: app.dockerImage, version: app.version },
|
||||
timeout: 180000
|
||||
})
|
||||
|
||||
// Update progress
|
||||
installingApps.value.set(app.id, {
|
||||
...installingApps.value.get(app.id)!,
|
||||
status: 'installing',
|
||||
progress: 60,
|
||||
message: 'Starting container...'
|
||||
})
|
||||
|
||||
// Wait for installation to complete (poll for package to appear)
|
||||
const checkInstalled = setInterval(() => {
|
||||
const current = installingApps.value.get(app.id)
|
||||
if (!current) {
|
||||
clearInterval(checkInstalled)
|
||||
return
|
||||
}
|
||||
|
||||
const newAttempt = current.attempt + 1
|
||||
installingApps.value.set(app.id, {
|
||||
...current,
|
||||
attempt: newAttempt,
|
||||
progress: Math.min(60 + (newAttempt * 0.5), 95),
|
||||
message: 'Initializing application...'
|
||||
})
|
||||
|
||||
if (isInstalled(app.id)) {
|
||||
clearInterval(checkInstalled)
|
||||
installingApps.value.set(app.id, {
|
||||
...current,
|
||||
status: 'complete',
|
||||
progress: 100,
|
||||
message: 'Installation complete!'
|
||||
})
|
||||
setTimeout(() => {
|
||||
installingApps.value.delete(app.id)
|
||||
}, 2000)
|
||||
} else if (newAttempt >= maxAttempts.value) {
|
||||
clearInterval(checkInstalled)
|
||||
installingApps.value.set(app.id, {
|
||||
...current,
|
||||
status: 'error',
|
||||
progress: 0,
|
||||
message: 'Installation timeout'
|
||||
})
|
||||
setTimeout(() => {
|
||||
installingApps.value.delete(app.id)
|
||||
}, 5000)
|
||||
}
|
||||
}, 1000)
|
||||
installingApps.value.set(app.id, { ...installingApps.value.get(app.id)!, status: 'installing', progress: 60, message: 'Starting container...' })
|
||||
|
||||
startInstallPolling(app.id, 'Initializing application...')
|
||||
} catch (err) {
|
||||
console.error('[Marketplace] Installation failed:', err)
|
||||
installingApps.value.set(app.id, {
|
||||
...installingApps.value.get(app.id)!,
|
||||
status: 'error',
|
||||
progress: 0,
|
||||
message: `Failed: ${err}`
|
||||
})
|
||||
setTimeout(() => {
|
||||
installingApps.value.delete(app.id)
|
||||
}, 5000)
|
||||
installingApps.value.set(app.id, { ...installingApps.value.get(app.id)!, status: 'error', progress: 0, message: `Failed: ${err}` })
|
||||
trackTimeout(() => { installingApps.value.delete(app.id) }, 5000)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1095,24 +1008,15 @@ async function sideloadPackage() {
|
||||
sideloadSuccess.value = ''
|
||||
|
||||
try {
|
||||
console.log(`Sideloading package from ${sideloadUrl.value}...`)
|
||||
|
||||
await rpcClient.call({
|
||||
method: 'package.sideload',
|
||||
params: {
|
||||
url: sideloadUrl.value
|
||||
}
|
||||
})
|
||||
await rpcClient.call({ method: 'package.sideload', params: { url: sideloadUrl.value } })
|
||||
|
||||
sideloadSuccess.value = 'Package installed successfully!'
|
||||
sideloadUrl.value = ''
|
||||
|
||||
// Close modal and navigate to apps page after short delay
|
||||
setTimeout(() => {
|
||||
trackTimeout(() => {
|
||||
showSideloadModal.value = false
|
||||
router.push('/dashboard/apps')
|
||||
router.push('/dashboard/apps').catch(() => {})
|
||||
}, 1500)
|
||||
|
||||
} catch (err: any) {
|
||||
console.error('Sideload failed:', err)
|
||||
sideloadError.value = err.message || 'Failed to install package'
|
||||
|
||||
Reference in New Issue
Block a user