fix: prevent My Apps crash when installing apps + add filebrowser to demo
The My Apps page went blank after installing apps because pkg['static-files'].icon was accessed without optional chaining on dynamically installed packages that lack the static-files property. - Make static-files optional in PackageDataEntry type - Add defensive ?.icon access with fallback in Apps.vue and AppDetails.vue - Add filebrowser to mock backend staticDevApps (enables Cloud page in demo) - Expand portMappings and marketplaceMetadata for all marketplace apps - installPackage now uses staticApp() format for consistent data shape Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
9c7ffbb263
commit
a2aa9657b1
@@ -28,7 +28,7 @@
|
||||
<div class="hidden md:flex items-center gap-6">
|
||||
<!-- App Icon -->
|
||||
<img
|
||||
:src="pkg['static-files'].icon"
|
||||
:src="pkg['static-files']?.icon || `/assets/img/app-icons/${pkg.manifest?.id || appId}.png`"
|
||||
:alt="pkg.manifest.title"
|
||||
class="w-20 h-20 rounded-xl shadow-xl flex-shrink-0"
|
||||
@error="handleImageError"
|
||||
@@ -120,7 +120,7 @@
|
||||
<div class="flex items-start gap-4 mb-4">
|
||||
<!-- App Icon -->
|
||||
<img
|
||||
:src="pkg['static-files'].icon"
|
||||
:src="pkg['static-files']?.icon || `/assets/img/app-icons/${pkg.manifest?.id || appId}.png`"
|
||||
:alt="pkg.manifest.title"
|
||||
class="w-20 h-20 rounded-xl shadow-xl flex-shrink-0"
|
||||
@error="handleImageError"
|
||||
|
||||
@@ -62,8 +62,8 @@
|
||||
|
||||
<div class="flex items-start gap-4">
|
||||
<img
|
||||
:src="pkg['static-files'].icon"
|
||||
:alt="pkg.manifest.title"
|
||||
:src="pkg['static-files']?.icon || `/assets/img/app-icons/${id}.png`"
|
||||
:alt="pkg.manifest?.title || String(id)"
|
||||
class="w-16 h-16 rounded-lg object-cover bg-white/10"
|
||||
@error="handleImageError"
|
||||
/>
|
||||
@@ -72,7 +72,7 @@
|
||||
{{ pkg.manifest.title }}
|
||||
</h3>
|
||||
<p class="text-sm text-white/70 mb-2 truncate">
|
||||
{{ pkg.manifest.description.short }}
|
||||
{{ pkg.manifest?.description?.short || '' }}
|
||||
</p>
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
|
||||
@@ -112,6 +112,7 @@
|
||||
@navigate="cloudStore.navigate($event)"
|
||||
@delete="handleDelete"
|
||||
@play="handlePlay"
|
||||
@share="handleShare"
|
||||
/>
|
||||
|
||||
<!-- Mini Audio Player -->
|
||||
@@ -152,6 +153,15 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Share Modal -->
|
||||
<ShareModal
|
||||
v-if="shareTarget"
|
||||
:filename="shareTarget.name"
|
||||
:filepath="shareTarget.path"
|
||||
:is-dir="shareTarget.isDir"
|
||||
@close="shareTarget = null"
|
||||
@saved="shareTarget = null"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -162,6 +172,7 @@ import { useAppStore } from '../stores/app'
|
||||
import { useCloudStore } from '../stores/cloud'
|
||||
import CloudToolbar from '../components/cloud/CloudToolbar.vue'
|
||||
import FileGrid from '../components/cloud/FileGrid.vue'
|
||||
import ShareModal from '../components/cloud/ShareModal.vue'
|
||||
import { useAudioPlayer } from '../composables/useAudioPlayer'
|
||||
|
||||
const router = useRouter()
|
||||
@@ -284,6 +295,12 @@ watch(useNativeUI, async (native) => {
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
const shareTarget = ref<{ path: string; name: string; isDir: boolean } | null>(null)
|
||||
|
||||
function handleShare(path: string, name: string, isDir: boolean) {
|
||||
shareTarget.value = { path, name, isDir }
|
||||
}
|
||||
|
||||
const uploadError = ref<string | null>(null)
|
||||
const draggingOver = ref(false)
|
||||
let dragLeaveTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
@@ -483,20 +483,17 @@ const isResettingOnboarding = ref(false)
|
||||
async function restartOnboarding() {
|
||||
if (isResettingOnboarding.value) return
|
||||
isResettingOnboarding.value = true
|
||||
try {
|
||||
await rpcClient.resetOnboarding()
|
||||
localStorage.removeItem('neode_onboarding_complete')
|
||||
localStorage.removeItem('neode_did')
|
||||
localStorage.removeItem('neode_did_state')
|
||||
localStorage.removeItem('neode_backup_created')
|
||||
await router.push('/onboarding/intro')
|
||||
// Local-only reset — no RPC needed since user isn't logged in.
|
||||
// Onboarding pages are all public, so clearing localStorage is enough.
|
||||
localStorage.removeItem('neode_onboarding_complete')
|
||||
localStorage.removeItem('neode_did')
|
||||
localStorage.removeItem('neode_did_state')
|
||||
localStorage.removeItem('neode_backup_created')
|
||||
router.push('/onboarding/intro').then(() => {
|
||||
window.location.reload()
|
||||
} catch (err) {
|
||||
console.error('Failed to reset onboarding:', err)
|
||||
error.value = err instanceof Error ? err.message : 'Failed to reset onboarding'
|
||||
} finally {
|
||||
isResettingOnboarding.value = false
|
||||
}
|
||||
}).catch(() => {
|
||||
window.location.href = '/onboarding/intro'
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="max-w-2xl w-full">
|
||||
<div class="glass-card p-12 pt-20 text-center relative overflow-visible onb-card">
|
||||
<!-- Logo - half in, half out of container -->
|
||||
<div class="absolute -top-10 left-1/2 -translate-x-1/2 z-10 onb-logo">
|
||||
<div class="absolute -top-10 left-0 right-0 flex justify-center z-10 onb-logo">
|
||||
<div class="logo-gradient-border w-20 h-20">
|
||||
<AnimatedLogo no-border fit />
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user