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:
Dorian
2026-03-01 18:07:35 +00:00
parent 7a05e11834
commit 94eb1e4283
7 changed files with 218 additions and 399 deletions
+76 -172
View File
@@ -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'