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:
@@ -621,6 +621,7 @@ function goBack() {
|
||||
router.back()
|
||||
}
|
||||
|
||||
|
||||
function launchApp() {
|
||||
if (!pkg.value) return
|
||||
|
||||
@@ -771,8 +772,7 @@ async function confirmUninstall() {
|
||||
|
||||
try {
|
||||
await store.uninstallPackage(appId.value)
|
||||
// Navigate back to apps after uninstall
|
||||
router.push('/dashboard/apps')
|
||||
router.push('/dashboard/apps').catch(() => {})
|
||||
} catch (err) {
|
||||
console.error('Failed to uninstall app:', err)
|
||||
alert('Failed to uninstall app')
|
||||
|
||||
@@ -171,7 +171,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed, ref, onBeforeUnmount } from 'vue'
|
||||
import { useRouter, RouterLink } from 'vue-router'
|
||||
import { useAppStore } from '../stores/app'
|
||||
import { useAppLauncherStore } from '../stores/appLauncher'
|
||||
@@ -187,7 +187,7 @@ const loadingActions = ref<Record<string, boolean>>({})
|
||||
// Use real packages from store - no more dummy apps
|
||||
const packages = computed(() => {
|
||||
const realPackages = store.packages
|
||||
console.log('[Apps] Real packages from store:', Object.keys(realPackages || {}).length, 'apps')
|
||||
if (import.meta.env.DEV) console.log('[Apps] Real packages from store:', Object.keys(realPackages || {}).length, 'apps')
|
||||
return realPackages || {}
|
||||
})
|
||||
|
||||
@@ -276,7 +276,7 @@ function launchApp(id: string) {
|
||||
}
|
||||
|
||||
// For other apps, navigate to app details which has launch functionality
|
||||
router.push(`/dashboard/apps/${id}`)
|
||||
router.push(`/dashboard/apps/${id}`).catch(() => {})
|
||||
}
|
||||
|
||||
function getStatusClass(state: PackageState): string {
|
||||
@@ -297,19 +297,20 @@ function getStatusClass(state: PackageState): string {
|
||||
}
|
||||
|
||||
function goToApp(id: string) {
|
||||
router.push(`/dashboard/apps/${id}`)
|
||||
router.push(`/dashboard/apps/${id}`).catch(() => {})
|
||||
}
|
||||
|
||||
const actionTimers = new Map<string, ReturnType<typeof setTimeout>>()
|
||||
|
||||
async function startApp(id: string) {
|
||||
loadingActions.value[id] = true
|
||||
try {
|
||||
await store.startPackage(id)
|
||||
// Wait for state update from WebSocket
|
||||
// The loader will be cleared when we receive the updated state
|
||||
// For now, keep a max timeout as fallback
|
||||
setTimeout(() => {
|
||||
if (actionTimers.has(id)) clearTimeout(actionTimers.get(id)!)
|
||||
actionTimers.set(id, setTimeout(() => {
|
||||
loadingActions.value[id] = false
|
||||
}, 5000)
|
||||
actionTimers.delete(id)
|
||||
}, 5000))
|
||||
} catch (err) {
|
||||
console.error('Failed to start app:', err)
|
||||
loadingActions.value[id] = false
|
||||
@@ -320,18 +321,22 @@ async function stopApp(id: string) {
|
||||
loadingActions.value[id] = true
|
||||
try {
|
||||
await store.stopPackage(id)
|
||||
// Wait for state update from WebSocket
|
||||
// The loader will be cleared when we receive the updated state
|
||||
// For now, keep a max timeout as fallback
|
||||
setTimeout(() => {
|
||||
if (actionTimers.has(id)) clearTimeout(actionTimers.get(id)!)
|
||||
actionTimers.set(id, setTimeout(() => {
|
||||
loadingActions.value[id] = false
|
||||
}, 5000)
|
||||
actionTimers.delete(id)
|
||||
}, 5000))
|
||||
} catch (err) {
|
||||
console.error('Failed to stop app:', err)
|
||||
loadingActions.value[id] = false
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
for (const t of actionTimers.values()) clearTimeout(t)
|
||||
actionTimers.clear()
|
||||
})
|
||||
|
||||
// @ts-ignore - Function kept for future use
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
|
||||
@@ -2,33 +2,33 @@
|
||||
<div class="min-h-screen flex relative dashboard-view" :class="{ 'glass-throw-active': showZoomIn }">
|
||||
<!-- Background container with 3D perspective - full width to avoid letterboxing -->
|
||||
<div class="bg-perspective-container">
|
||||
<!-- Background - default (zoom animates on this layer, not container, to avoid letterboxing) -->
|
||||
<!-- Background - primary layer (visible for all routes, transitions out only for detail pages) -->
|
||||
<div
|
||||
ref="bgDefault"
|
||||
class="bg-layer bg-fullwidth"
|
||||
:class="[
|
||||
{ 'bg-transitioning-out': showAltBackground || showWeb5Background || showNetworkBackground || showSettingsBackground || showMyAppsBackground || showAppStoreBackground || showCloudBackground || showHomeBackground },
|
||||
{ 'bg-transitioning-out': showAltBackground },
|
||||
{ 'zoom-reveal-bg': showZoomIn }
|
||||
]"
|
||||
:style="{ backgroundImage: `url(/assets/img/${currentBackgroundImage})` }"
|
||||
:style="{ backgroundImage: `url(/assets/img/${backgroundImage})` }"
|
||||
/>
|
||||
<!-- Background - alternate for app details and Web5 -->
|
||||
<!-- Background - detail layer (only visible during app/marketplace detail 3D transition) -->
|
||||
<div
|
||||
ref="bgAlt"
|
||||
class="bg-layer bg-fullwidth"
|
||||
:class="{ 'bg-transitioning-in': showAltBackground || showWeb5Background || showNetworkBackground || showSettingsBackground || showMyAppsBackground || showAppStoreBackground || showCloudBackground || showHomeBackground }"
|
||||
:style="{ backgroundImage: `url(/assets/img/${altBackgroundImage})` }"
|
||||
:class="{ 'bg-transitioning-in': showAltBackground }"
|
||||
style="background-image: url(/assets/img/bg-intro-3.jpg)"
|
||||
/>
|
||||
<!-- Glitch overlays - trigger on background change -->
|
||||
<div
|
||||
class="bg-glitch-layer-1"
|
||||
:class="{ 'glitch-active': isGlitching }"
|
||||
:style="{ backgroundImage: `url(/assets/img/${altBackgroundImage})` }"
|
||||
:style="{ backgroundImage: `url(/assets/img/${backgroundImage})` }"
|
||||
/>
|
||||
<div
|
||||
class="bg-glitch-layer-2"
|
||||
:class="{ 'glitch-active': isGlitching }"
|
||||
:style="{ backgroundImage: `url(/assets/img/${altBackgroundImage})` }"
|
||||
:style="{ backgroundImage: `url(/assets/img/${backgroundImage})` }"
|
||||
/>
|
||||
<div
|
||||
class="bg-glitch-scan"
|
||||
@@ -37,11 +37,11 @@
|
||||
<!-- Continuous glitch/flash overlays - same as login, every 5s -->
|
||||
<div
|
||||
class="dashboard-glitch-layer dashboard-glitch-1 bg-fullwidth"
|
||||
:style="{ backgroundImage: `url(/assets/img/${currentBackgroundImage})` }"
|
||||
:style="{ backgroundImage: `url(/assets/img/${backgroundImage})` }"
|
||||
/>
|
||||
<div
|
||||
class="dashboard-glitch-layer dashboard-glitch-2 bg-fullwidth"
|
||||
:style="{ backgroundImage: `url(/assets/img/${currentBackgroundImage})` }"
|
||||
:style="{ backgroundImage: `url(/assets/img/${backgroundImage})` }"
|
||||
/>
|
||||
<div class="dashboard-glitch-scan" />
|
||||
</div>
|
||||
@@ -319,52 +319,61 @@ const store = useAppStore()
|
||||
const loginTransition = useLoginTransitionStore()
|
||||
|
||||
const showZoomIn = ref(false)
|
||||
const pendingTimers: ReturnType<typeof setTimeout>[] = []
|
||||
|
||||
function scheduledTimeout(fn: () => void, ms: number) {
|
||||
const id = setTimeout(fn, ms)
|
||||
pendingTimers.push(id)
|
||||
return id
|
||||
}
|
||||
|
||||
function isDetailRoute(path: string) {
|
||||
return (path.includes('/apps/') && !path.endsWith('/apps')) ||
|
||||
(path.includes('/marketplace/') && !path.endsWith('/marketplace'))
|
||||
}
|
||||
|
||||
// Background swap for app details, Web5, Network, Settings, My Apps, App Store, Cloud, and Home
|
||||
const showAltBackground = ref(false)
|
||||
const isHomeRoute = computed(() => route.path === '/dashboard' || route.path === '/dashboard/')
|
||||
const showHomeBackground = ref(route.path === '/dashboard' || route.path === '/dashboard/')
|
||||
const showWeb5Background = ref(route.path.includes('/dashboard/web5'))
|
||||
const showNetworkBackground = ref(route.path.includes('/dashboard/server'))
|
||||
const showSettingsBackground = ref(route.path.includes('/dashboard/settings'))
|
||||
const showMyAppsBackground = ref(route.path.includes('/dashboard/apps') && !route.path.includes('/dashboard/apps/'))
|
||||
const showAppStoreBackground = ref(route.path.includes('/dashboard/marketplace'))
|
||||
const showCloudBackground = ref(route.path.includes('/dashboard/cloud'))
|
||||
const showWeb5Overlay = ref(route.path.includes('/dashboard/web5')) // Separate ref for overlay to handle transition delay
|
||||
const showNetworkOverlay = ref(route.path.includes('/dashboard/server'))
|
||||
const showSettingsOverlay = ref(route.path.includes('/dashboard/settings'))
|
||||
const showMyAppsOverlay = ref(route.path.includes('/dashboard/apps') && !route.path.includes('/dashboard/apps/'))
|
||||
const showAppStoreOverlay = ref(route.path.includes('/dashboard/marketplace'))
|
||||
const showCloudOverlay = ref(route.path.includes('/dashboard/cloud'))
|
||||
const isGlitching = ref(false)
|
||||
|
||||
// Background images
|
||||
const currentBackgroundImage = computed(() => {
|
||||
if (showWeb5Background.value) return 'bg-web5.jpg'
|
||||
if (showNetworkBackground.value) return 'bg-network.jpg'
|
||||
if (showSettingsBackground.value) return 'bg-settings.jpg'
|
||||
if (showMyAppsBackground.value) return 'bg-myapps.jpg'
|
||||
if (showAppStoreBackground.value) return 'bg-appstore.jpg'
|
||||
if (showCloudBackground.value) return 'bg-cloud.jpg'
|
||||
if (showHomeBackground.value) return 'bg-home.jpg'
|
||||
return 'bg-intro.jpg'
|
||||
const ROUTE_BACKGROUNDS: Record<string, string> = {
|
||||
'/dashboard': 'bg-home.jpg',
|
||||
'/dashboard/': 'bg-home.jpg',
|
||||
'/dashboard/apps': 'bg-myapps.jpg',
|
||||
'/dashboard/marketplace': 'bg-appstore.jpg',
|
||||
'/dashboard/cloud': 'bg-cloud.jpg',
|
||||
'/dashboard/server': 'bg-network.jpg',
|
||||
'/dashboard/web5': 'bg-web5.jpg',
|
||||
'/dashboard/settings': 'bg-settings.jpg',
|
||||
}
|
||||
|
||||
const backgroundImage = computed(() => {
|
||||
if (isDetailRoute(route.path)) return 'bg-intro.jpg'
|
||||
return ROUTE_BACKGROUNDS[route.path] || 'bg-home.jpg'
|
||||
})
|
||||
|
||||
const altBackgroundImage = computed(() => {
|
||||
if (showWeb5Background.value) return 'bg-web5.jpg'
|
||||
if (showNetworkBackground.value) return 'bg-network.jpg'
|
||||
if (showSettingsBackground.value) return 'bg-settings.jpg'
|
||||
if (showMyAppsBackground.value) return 'bg-myapps.jpg'
|
||||
if (showAppStoreBackground.value) return 'bg-appstore.jpg'
|
||||
if (showCloudBackground.value) return 'bg-cloud.jpg'
|
||||
if (showHomeBackground.value) return 'bg-home.jpg'
|
||||
return 'bg-intro-3.jpg'
|
||||
const isDarkRoute = computed(() => {
|
||||
const p = route.path
|
||||
return p.includes('/dashboard/web5') ||
|
||||
p.includes('/dashboard/server') ||
|
||||
p.includes('/dashboard/settings') ||
|
||||
(p.includes('/dashboard/apps') && !isDetailRoute(p)) ||
|
||||
p.includes('/dashboard/marketplace') ||
|
||||
p.includes('/dashboard/cloud')
|
||||
})
|
||||
|
||||
// Check if overlay should be dark (0.8 opacity)
|
||||
const showDarkOverlay = computed(() => {
|
||||
return showWeb5Overlay.value || showNetworkOverlay.value || showSettingsOverlay.value || showMyAppsOverlay.value || showAppStoreOverlay.value || showCloudOverlay.value
|
||||
const showDarkOverlay = ref(isDarkRoute.value)
|
||||
let overlayTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
watch(isDarkRoute, (dark) => {
|
||||
if (overlayTimer) { clearTimeout(overlayTimer); overlayTimer = null }
|
||||
if (dark) {
|
||||
showDarkOverlay.value = true
|
||||
} else {
|
||||
overlayTimer = scheduledTimeout(() => { showDarkOverlay.value = false }, 450)
|
||||
}
|
||||
})
|
||||
|
||||
const mobileTabBar = ref<HTMLElement | null>(null)
|
||||
const appsTabRef = ref<HTMLElement | null>(null)
|
||||
const marketplaceTabRef = ref<HTMLElement | null>(null)
|
||||
@@ -376,116 +385,17 @@ const cloudTabRef = ref<HTMLElement | null>(null)
|
||||
const networkTabIndicatorLeft = ref(0)
|
||||
const networkTabIndicatorWidth = ref(0)
|
||||
|
||||
function isDetailRoute(path: string) {
|
||||
return (path.includes('/apps/') && !path.endsWith('/apps')) ||
|
||||
(path.includes('/marketplace/') && !path.endsWith('/marketplace'))
|
||||
}
|
||||
|
||||
watch(() => route.path, (newPath) => {
|
||||
// Check if we're on app details OR marketplace app details
|
||||
const isAppDetails = isDetailRoute(newPath)
|
||||
const wasAppDetails = showAltBackground.value
|
||||
|
||||
// Check if we're on special background routes
|
||||
const isHome = newPath === '/dashboard' || newPath === '/dashboard/'
|
||||
const isWeb5 = newPath.includes('/dashboard/web5')
|
||||
const wasWeb5 = showWeb5Background.value
|
||||
const isNetwork = newPath.includes('/dashboard/server')
|
||||
const wasNetwork = showNetworkBackground.value
|
||||
const isSettings = newPath.includes('/dashboard/settings')
|
||||
const wasSettings = showSettingsBackground.value
|
||||
const isMyApps = newPath.includes('/dashboard/apps') && !newPath.includes('/dashboard/apps/')
|
||||
const wasMyApps = showMyAppsBackground.value
|
||||
const isAppStore = newPath.includes('/dashboard/marketplace')
|
||||
const wasAppStore = showAppStoreBackground.value
|
||||
const isCloud = newPath.includes('/dashboard/cloud')
|
||||
const wasCloud = showCloudBackground.value
|
||||
|
||||
// Change background immediately
|
||||
|
||||
showAltBackground.value = isAppDetails
|
||||
showHomeBackground.value = isHome
|
||||
showWeb5Background.value = isWeb5
|
||||
showNetworkBackground.value = isNetwork
|
||||
showSettingsBackground.value = isSettings
|
||||
showMyAppsBackground.value = isMyApps
|
||||
showAppStoreBackground.value = isAppStore
|
||||
showCloudBackground.value = isCloud
|
||||
|
||||
// Handle overlay transitions with delay when leaving special backgrounds
|
||||
// Web5 overlay
|
||||
if (isWeb5) {
|
||||
showWeb5Overlay.value = true
|
||||
} else if (wasWeb5 && !isWeb5) {
|
||||
setTimeout(() => {
|
||||
showWeb5Overlay.value = false
|
||||
}, 450)
|
||||
} else {
|
||||
showWeb5Overlay.value = isWeb5
|
||||
}
|
||||
|
||||
// Network overlay
|
||||
if (isNetwork) {
|
||||
showNetworkOverlay.value = true
|
||||
} else if (wasNetwork && !isNetwork) {
|
||||
setTimeout(() => {
|
||||
showNetworkOverlay.value = false
|
||||
}, 450)
|
||||
} else {
|
||||
showNetworkOverlay.value = isNetwork
|
||||
}
|
||||
|
||||
// Settings overlay
|
||||
if (isSettings) {
|
||||
showSettingsOverlay.value = true
|
||||
} else if (wasSettings && !isSettings) {
|
||||
setTimeout(() => {
|
||||
showSettingsOverlay.value = false
|
||||
}, 450)
|
||||
} else {
|
||||
showSettingsOverlay.value = isSettings
|
||||
}
|
||||
|
||||
// My Apps overlay
|
||||
if (isMyApps) {
|
||||
showMyAppsOverlay.value = true
|
||||
} else if (wasMyApps && !isMyApps) {
|
||||
setTimeout(() => {
|
||||
showMyAppsOverlay.value = false
|
||||
}, 450)
|
||||
} else {
|
||||
showMyAppsOverlay.value = isMyApps
|
||||
}
|
||||
|
||||
// App Store overlay
|
||||
if (isAppStore) {
|
||||
showAppStoreOverlay.value = true
|
||||
} else if (wasAppStore && !isAppStore) {
|
||||
setTimeout(() => {
|
||||
showAppStoreOverlay.value = false
|
||||
}, 450)
|
||||
} else {
|
||||
showAppStoreOverlay.value = isAppStore
|
||||
}
|
||||
|
||||
// Cloud overlay
|
||||
if (isCloud) {
|
||||
showCloudOverlay.value = true
|
||||
} else if (wasCloud && !isCloud) {
|
||||
setTimeout(() => {
|
||||
showCloudOverlay.value = false
|
||||
}, 450)
|
||||
} else {
|
||||
showCloudOverlay.value = isCloud
|
||||
}
|
||||
|
||||
// Trigger glitch ONLY when going forward (to app details), not back
|
||||
|
||||
if (isAppDetails && !wasAppDetails) {
|
||||
setTimeout(() => {
|
||||
scheduledTimeout(() => {
|
||||
isGlitching.value = true
|
||||
setTimeout(() => {
|
||||
isGlitching.value = false
|
||||
}, 375) // Glitch duration - 25% faster
|
||||
}, 500) // Wait for background 3D transition to complete
|
||||
scheduledTimeout(() => { isGlitching.value = false }, 375)
|
||||
}, 500)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -550,6 +460,12 @@ function updateNetworkTabIndicator() {
|
||||
networkTabIndicatorWidth.value = tabRect.width
|
||||
}
|
||||
|
||||
function onResize() {
|
||||
updateTabBarHeight()
|
||||
updateAppsTabIndicator()
|
||||
updateNetworkTabIndicator()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.body.classList.add('dashboard-active')
|
||||
if (loginTransition.justLoggedIn) {
|
||||
@@ -557,39 +473,30 @@ onMounted(() => {
|
||||
showZoomIn.value = true
|
||||
loginTransition.setPendingWelcomeTyping(true)
|
||||
loginTransition.setJustLoggedIn(false)
|
||||
// Trigger glitches during background reveal (0.5s, 1.2s, 2s into 2.8s zoom)
|
||||
const triggerRevealGlitch = () => {
|
||||
isGlitching.value = true
|
||||
setTimeout(() => { isGlitching.value = false }, 380)
|
||||
scheduledTimeout(() => { isGlitching.value = false }, 380)
|
||||
}
|
||||
setTimeout(triggerRevealGlitch, 500)
|
||||
setTimeout(triggerRevealGlitch, 1200)
|
||||
setTimeout(triggerRevealGlitch, 2000)
|
||||
// Keep glass-throw active long enough for sidebar (last to animate, ~7.5s)
|
||||
setTimeout(() => {
|
||||
showZoomIn.value = false
|
||||
}, 8000)
|
||||
// Trigger welcome typing on Home after main content animation finishes
|
||||
setTimeout(() => {
|
||||
scheduledTimeout(triggerRevealGlitch, 500)
|
||||
scheduledTimeout(triggerRevealGlitch, 1200)
|
||||
scheduledTimeout(triggerRevealGlitch, 2000)
|
||||
scheduledTimeout(() => { showZoomIn.value = false }, 8000)
|
||||
scheduledTimeout(() => {
|
||||
loginTransition.setStartWelcomeTyping(true)
|
||||
loginTransition.setPendingWelcomeTyping(false)
|
||||
}, 4000)
|
||||
}
|
||||
|
||||
updateTabBarHeight()
|
||||
updateAppsTabIndicator()
|
||||
updateNetworkTabIndicator()
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
updateTabBarHeight()
|
||||
updateAppsTabIndicator()
|
||||
updateNetworkTabIndicator()
|
||||
})
|
||||
onResize()
|
||||
window.addEventListener('resize', onResize)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.body.classList.remove('dashboard-active')
|
||||
window.removeEventListener('resize', updateTabBarHeight)
|
||||
window.removeEventListener('resize', onResize)
|
||||
for (const id of pendingTimers) clearTimeout(id)
|
||||
pendingTimers.length = 0
|
||||
if (overlayTimer) { clearTimeout(overlayTimer); overlayTimer = null }
|
||||
})
|
||||
|
||||
// Watch route changes to update indicator position
|
||||
@@ -676,14 +583,6 @@ const mobileNavItems = [
|
||||
},
|
||||
]
|
||||
|
||||
// Use appropriate nav items based on screen size
|
||||
// @ts-ignore - Computed kept for future use
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const navItems = computed(() => {
|
||||
if (typeof window === 'undefined') return desktopNavItems
|
||||
return window.innerWidth >= 768 ? desktopNavItems : mobileNavItems
|
||||
})
|
||||
|
||||
function getIconPath(iconName: string): string[] {
|
||||
const icons: Record<string, string[]> = {
|
||||
home: ['M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6'],
|
||||
@@ -846,9 +745,6 @@ function getTransitionName(currentRoute: any) {
|
||||
.zoom-reveal-bg {
|
||||
animation: zoom-reveal 2.8s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
|
||||
transform-origin: center center;
|
||||
}
|
||||
|
||||
.zoom-reveal-bg {
|
||||
opacity: 0;
|
||||
transform: scale(0.15);
|
||||
filter: blur(24px);
|
||||
|
||||
@@ -130,7 +130,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 AnimatedLogo from '@/components/AnimatedLogo.vue'
|
||||
import { useAppStore } from '../stores/app'
|
||||
@@ -154,21 +154,32 @@ const isSetupMode = computed(() => {
|
||||
return import.meta.env.VITE_DEV_MODE === 'setup'
|
||||
})
|
||||
|
||||
let unlockHandler: (() => void) | null = null
|
||||
|
||||
function removeUnlockListeners() {
|
||||
if (unlockHandler) {
|
||||
document.removeEventListener('click', unlockHandler)
|
||||
document.removeEventListener('touchstart', unlockHandler)
|
||||
document.removeEventListener('keydown', unlockHandler)
|
||||
unlockHandler = null
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeUnmount(removeUnlockListeners)
|
||||
|
||||
onMounted(async () => {
|
||||
const fromSplash = sessionStorage.getItem('archipelago_from_splash') === '1'
|
||||
if (fromSplash) sessionStorage.removeItem('archipelago_from_splash')
|
||||
const unlock = () => {
|
||||
unlockHandler = () => {
|
||||
if (!fromSplash) {
|
||||
resumeAudioContext()
|
||||
startSynthwave()
|
||||
}
|
||||
document.removeEventListener('click', unlock)
|
||||
document.removeEventListener('touchstart', unlock)
|
||||
document.removeEventListener('keydown', unlock)
|
||||
removeUnlockListeners()
|
||||
}
|
||||
document.addEventListener('click', unlock, { once: true })
|
||||
document.addEventListener('touchstart', unlock, { once: true })
|
||||
document.addEventListener('keydown', unlock, { once: true })
|
||||
document.addEventListener('click', unlockHandler, { once: true })
|
||||
document.addEventListener('touchstart', unlockHandler, { once: true })
|
||||
document.addEventListener('keydown', unlockHandler, { once: true })
|
||||
if (isSetupMode.value) {
|
||||
try {
|
||||
const result = await rpcClient.call<boolean>({ method: 'auth.isSetup', params: {}, timeout: 8000 })
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -326,7 +326,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useAppStore } from '../stores/app'
|
||||
import { rpcClient } from '../api/rpc-client'
|
||||
@@ -392,48 +392,50 @@ const features = computed(() => {
|
||||
]
|
||||
})
|
||||
|
||||
let pendingRedirect: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
onMounted(() => {
|
||||
console.log('[MarketplaceAppDetails] Loading app ID:', appId.value)
|
||||
if (import.meta.env.DEV) console.log('[MarketplaceAppDetails] Loading app ID:', appId.value)
|
||||
|
||||
try {
|
||||
// Get app data from composable
|
||||
const loadedApp = getCurrentApp()
|
||||
|
||||
if (loadedApp && loadedApp.id === appId.value) {
|
||||
app.value = loadedApp
|
||||
console.log('[MarketplaceAppDetails] App loaded successfully:', app.value)
|
||||
loading.value = false
|
||||
} else {
|
||||
console.warn('[MarketplaceAppDetails] App data not found in composable')
|
||||
loading.value = false
|
||||
// Navigate back to marketplace
|
||||
setTimeout(() => {
|
||||
router.push('/dashboard/marketplace')
|
||||
pendingRedirect = setTimeout(() => {
|
||||
router.push('/dashboard/marketplace').catch(() => {})
|
||||
}, 500)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[MarketplaceAppDetails] Error loading app data:', e)
|
||||
loading.value = false
|
||||
setTimeout(() => {
|
||||
router.push('/dashboard/marketplace')
|
||||
pendingRedirect = setTimeout(() => {
|
||||
router.push('/dashboard/marketplace').catch(() => {})
|
||||
}, 500)
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (pendingRedirect) { clearTimeout(pendingRedirect); pendingRedirect = null }
|
||||
})
|
||||
|
||||
function handleImageError(e: Event) {
|
||||
const target = e.target as HTMLImageElement
|
||||
target.src = '/assets/img/logo-archipelago.svg'
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
router.push('/dashboard/marketplace')
|
||||
router.push('/dashboard/marketplace').catch(() => {})
|
||||
}
|
||||
|
||||
function goToInstalledApp() {
|
||||
router.push({
|
||||
path: `/dashboard/apps/${appId.value}`,
|
||||
query: { from: 'marketplace' }
|
||||
})
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
async function installApp() {
|
||||
@@ -478,8 +480,7 @@ async function installApp() {
|
||||
// Wait a moment for the package to be registered
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
// Navigate to the installed app
|
||||
router.push(`/dashboard/apps/${appId.value}`)
|
||||
router.push(`/dashboard/apps/${appId.value}`).catch(() => {})
|
||||
} catch (err: any) {
|
||||
installError.value = err.message || 'Installation failed. Please try again.'
|
||||
console.error('[MarketplaceAppDetails] Failed to install app:', err)
|
||||
|
||||
@@ -284,24 +284,26 @@ async function handleChangePassword() {
|
||||
}
|
||||
}
|
||||
|
||||
let copiedTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
async function copyOnionAddress() {
|
||||
const addr = serverTorAddress.value
|
||||
if (!addr) return
|
||||
try {
|
||||
await navigator.clipboard.writeText(addr)
|
||||
copiedOnion.value = true
|
||||
setTimeout(() => { copiedOnion.value = false }, 2000)
|
||||
} catch {
|
||||
// Fallback for older browsers
|
||||
const ta = document.createElement('textarea')
|
||||
ta.value = addr
|
||||
ta.style.position = 'fixed'
|
||||
ta.style.opacity = '0'
|
||||
document.body.appendChild(ta)
|
||||
ta.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(ta)
|
||||
copiedOnion.value = true
|
||||
setTimeout(() => { copiedOnion.value = false }, 2000)
|
||||
}
|
||||
copiedOnion.value = true
|
||||
if (copiedTimer) clearTimeout(copiedTimer)
|
||||
copiedTimer = setTimeout(() => { copiedOnion.value = false }, 2000)
|
||||
}
|
||||
|
||||
function closeChangePasswordModal() {
|
||||
@@ -324,7 +326,7 @@ onMounted(async () => {
|
||||
})
|
||||
|
||||
async function handleLogout() {
|
||||
await store.logout()
|
||||
router.push('/login')
|
||||
try { await store.logout() } catch { /* proceed */ }
|
||||
router.push('/login').catch(() => { window.location.href = '/login' })
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user