2026-01-24 22:59:20 +00:00
|
|
|
<template>
|
|
|
|
|
<div class="app-details-container pb-16 md:pb-16">
|
|
|
|
|
<!-- Desktop Back Button -->
|
|
|
|
|
<button @click="goBack" class="hidden md:flex mb-6 items-center gap-2 text-white/70 hover:text-white transition-colors">
|
|
|
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
|
|
|
|
</svg>
|
2026-03-11 13:45:59 +00:00
|
|
|
{{ t('marketplaceDetails.backToStore') }}
|
2026-01-24 22:59:20 +00:00
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
<!-- Mobile Full-Width Back Button -->
|
2026-03-11 13:45:59 +00:00
|
|
|
<button
|
2026-01-24 22:59:20 +00:00
|
|
|
@click="goBack"
|
|
|
|
|
class="md:hidden fixed left-4 right-4 z-40 glass-button px-6 py-3 rounded-lg font-medium shadow-2xl flex items-center justify-center gap-2"
|
|
|
|
|
:style="{
|
|
|
|
|
bottom: bottomPosition,
|
|
|
|
|
filter: 'drop-shadow(0 10px 25px rgba(0, 0, 0, 0.5))'
|
|
|
|
|
}"
|
|
|
|
|
>
|
|
|
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
|
|
|
|
</svg>
|
2026-03-11 13:45:59 +00:00
|
|
|
<span>{{ t('marketplaceDetails.backToStore') }}</span>
|
2026-01-24 22:59:20 +00:00
|
|
|
</button>
|
|
|
|
|
|
2026-03-09 07:43:12 +00:00
|
|
|
<Transition name="content-fade" mode="out-in">
|
2026-01-24 22:59:20 +00:00
|
|
|
<!-- Loading State -->
|
2026-03-09 07:43:12 +00:00
|
|
|
<div v-if="loading" key="loading" class="glass-card p-12 text-center">
|
2026-01-24 22:59:20 +00:00
|
|
|
<svg class="animate-spin h-12 w-12 text-blue-400 mx-auto mb-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
|
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
|
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
|
|
|
</svg>
|
2026-03-11 13:45:59 +00:00
|
|
|
<p class="text-white/70">{{ t('marketplaceDetails.loadingDetails') }}</p>
|
2026-01-24 22:59:20 +00:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- App Details -->
|
2026-03-09 07:43:12 +00:00
|
|
|
<div v-else-if="app" key="content">
|
2026-01-24 22:59:20 +00:00
|
|
|
<!-- Compact Hero Section -->
|
|
|
|
|
<div class="glass-card p-6 mb-6">
|
|
|
|
|
<!-- Desktop: Single Row Layout -->
|
|
|
|
|
<div class="hidden md:flex items-center gap-6">
|
|
|
|
|
<!-- App Icon -->
|
|
|
|
|
<img
|
|
|
|
|
v-if="app.icon"
|
|
|
|
|
:src="app.icon"
|
|
|
|
|
:alt="app.title"
|
|
|
|
|
class="w-20 h-20 rounded-xl shadow-xl flex-shrink-0"
|
|
|
|
|
@error="handleImageError"
|
|
|
|
|
/>
|
|
|
|
|
<div v-else class="w-20 h-20 rounded-xl bg-white/10 flex items-center justify-center flex-shrink-0">
|
|
|
|
|
<svg class="w-10 h-10 text-white/40" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
|
|
|
|
|
</svg>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- App Info (grows to fill space) -->
|
|
|
|
|
<div class="flex-1 min-w-0">
|
|
|
|
|
<h1 class="text-2xl font-bold text-white mb-1">{{ app.title }}</h1>
|
|
|
|
|
<p class="text-white/70 text-sm mb-2">{{ shortDescription }}</p>
|
|
|
|
|
<div class="flex items-center gap-2">
|
|
|
|
|
<span
|
|
|
|
|
v-if="isInstalled"
|
|
|
|
|
class="inline-flex items-center px-2.5 py-1 rounded-lg text-xs font-medium bg-green-500/20 text-green-200 border border-green-500/30"
|
|
|
|
|
>
|
|
|
|
|
<span class="w-1.5 h-1.5 rounded-full bg-green-400 mr-1.5"></span>
|
2026-03-11 13:45:59 +00:00
|
|
|
{{ t('marketplaceDetails.installed') }}
|
2026-01-24 22:59:20 +00:00
|
|
|
</span>
|
|
|
|
|
<span class="text-white/50 text-xs">{{ app.version ? `v${app.version}` : 'latest' }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-03-11 13:45:59 +00:00
|
|
|
|
2026-01-24 22:59:20 +00:00
|
|
|
<!-- Action Buttons -->
|
|
|
|
|
<div class="flex items-center gap-2 flex-shrink-0">
|
|
|
|
|
<button
|
|
|
|
|
v-if="isInstalled"
|
|
|
|
|
@click="goToInstalledApp"
|
2026-03-04 05:23:42 +00:00
|
|
|
class="glass-button glass-button-sm px-6 py-2.5 rounded-lg text-sm font-semibold flex items-center gap-2"
|
2026-01-24 22:59:20 +00:00
|
|
|
>
|
|
|
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
|
|
|
|
</svg>
|
2026-03-11 13:45:59 +00:00
|
|
|
{{ t('marketplaceDetails.open') }}
|
2026-01-24 22:59:20 +00:00
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
v-else
|
|
|
|
|
@click="installApp"
|
2026-05-05 11:29:18 -04:00
|
|
|
:disabled="installing || (!installBlockedReason && !app.manifestUrl && !app.dockerImage)"
|
|
|
|
|
:title="installBlockedReason || undefined"
|
2026-03-04 05:23:42 +00:00
|
|
|
class="glass-button glass-button-sm px-6 py-2.5 rounded-lg text-sm font-semibold flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
2026-01-24 22:59:20 +00:00
|
|
|
>
|
|
|
|
|
<svg v-if="installing" class="animate-spin h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
|
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
|
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
|
|
|
</svg>
|
|
|
|
|
<svg v-else class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
|
|
|
|
</svg>
|
2026-05-05 11:29:18 -04:00
|
|
|
{{ installBlockedReason ? 'Bitcoin Pruned' : installing ? t('common.installing') : t('common.install') }}
|
2026-01-24 22:59:20 +00:00
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Mobile: Two Column Grid Layout -->
|
|
|
|
|
<div class="md:hidden">
|
|
|
|
|
<!-- Top: Icon + Info -->
|
|
|
|
|
<div class="grid grid-cols-[80px_1fr] gap-4 mb-4">
|
|
|
|
|
<!-- App Icon -->
|
|
|
|
|
<img
|
|
|
|
|
v-if="app.icon"
|
|
|
|
|
:src="app.icon"
|
|
|
|
|
:alt="app.title"
|
|
|
|
|
class="w-20 h-20 rounded-xl shadow-xl"
|
|
|
|
|
@error="handleImageError"
|
|
|
|
|
/>
|
|
|
|
|
<div v-else class="w-20 h-20 rounded-xl bg-white/10 flex items-center justify-center">
|
|
|
|
|
<svg class="w-10 h-10 text-white/40" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
|
|
|
|
|
</svg>
|
|
|
|
|
</div>
|
2026-03-09 07:43:12 +00:00
|
|
|
|
2026-01-24 22:59:20 +00:00
|
|
|
<!-- App Info -->
|
|
|
|
|
<div class="min-w-0">
|
|
|
|
|
<h1 class="text-xl font-bold text-white mb-1">{{ app.title }}</h1>
|
|
|
|
|
<p class="text-white/70 text-xs mb-2 line-clamp-2">{{ shortDescription }}</p>
|
|
|
|
|
<div class="flex flex-wrap items-center gap-2">
|
|
|
|
|
<span
|
|
|
|
|
v-if="isInstalled"
|
|
|
|
|
class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-500/20 text-green-200 border border-green-500/30"
|
|
|
|
|
>
|
|
|
|
|
<span class="w-1.5 h-1.5 rounded-full bg-green-400 mr-1"></span>
|
2026-03-11 13:45:59 +00:00
|
|
|
{{ t('marketplaceDetails.installed') }}
|
2026-01-24 22:59:20 +00:00
|
|
|
</span>
|
|
|
|
|
<span class="text-white/50 text-xs">{{ app.version ? `v${app.version}` : 'latest' }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Bottom: Action Buttons -->
|
|
|
|
|
<div class="grid grid-cols-2 gap-2">
|
|
|
|
|
<button
|
|
|
|
|
v-if="isInstalled"
|
|
|
|
|
@click="goToInstalledApp"
|
2026-03-04 05:23:42 +00:00
|
|
|
class="glass-button glass-button-sm px-4 py-2.5 rounded-lg text-sm font-semibold flex items-center justify-center gap-2"
|
2026-01-24 22:59:20 +00:00
|
|
|
>
|
|
|
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
|
|
|
|
</svg>
|
2026-03-11 13:45:59 +00:00
|
|
|
{{ t('marketplaceDetails.open') }}
|
2026-01-24 22:59:20 +00:00
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
v-else
|
|
|
|
|
@click="installApp"
|
2026-05-05 11:29:18 -04:00
|
|
|
:disabled="installing || (!installBlockedReason && !app.manifestUrl && !app.dockerImage)"
|
|
|
|
|
:title="installBlockedReason || undefined"
|
2026-03-04 05:23:42 +00:00
|
|
|
class="glass-button glass-button-sm px-4 py-2.5 rounded-lg text-sm font-semibold flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed col-span-2"
|
2026-01-24 22:59:20 +00:00
|
|
|
>
|
|
|
|
|
<svg v-if="installing" class="animate-spin h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
|
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
|
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
|
|
|
</svg>
|
|
|
|
|
<svg v-else class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
|
|
|
|
</svg>
|
2026-05-05 11:29:18 -04:00
|
|
|
{{ installBlockedReason ? 'Bitcoin Pruned' : installing ? t('common.installing') : t('common.install') }}
|
2026-01-24 22:59:20 +00:00
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Installation Error Banner (Mobile) -->
|
|
|
|
|
<div v-if="installError" class="mt-4 p-3 bg-red-500/20 border border-red-500/40 rounded-lg">
|
|
|
|
|
<div class="flex items-start gap-2">
|
|
|
|
|
<svg class="w-4 h-4 text-red-400 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
|
|
|
</svg>
|
|
|
|
|
<div class="flex-1">
|
2026-03-11 13:45:59 +00:00
|
|
|
<p class="text-red-200 font-medium text-sm">{{ t('marketplaceDetails.installFailed') }}</p>
|
2026-01-24 22:59:20 +00:00
|
|
|
<p class="text-red-300 text-xs mt-1">{{ installError }}</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Installation Error Banner (Desktop) -->
|
|
|
|
|
<div v-if="installError" class="hidden md:block mt-4 p-4 bg-red-500/20 border border-red-500/40 rounded-lg">
|
|
|
|
|
<div class="flex items-start gap-3">
|
|
|
|
|
<svg class="w-5 h-5 text-red-400 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
|
|
|
</svg>
|
|
|
|
|
<div class="flex-1">
|
2026-03-11 13:45:59 +00:00
|
|
|
<p class="text-red-200 font-medium">{{ t('marketplaceDetails.installFailed') }}</p>
|
2026-01-24 22:59:20 +00:00
|
|
|
<p class="text-red-300 text-sm mt-1">{{ installError }}</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-05-05 11:29:18 -04:00
|
|
|
<div v-if="installBlockedReason" class="hidden md:block mt-4 p-4 bg-yellow-500/15 border border-yellow-500/30 rounded-lg">
|
|
|
|
|
<p class="text-yellow-100 font-medium">Bitcoin is in pruned mode</p>
|
|
|
|
|
<p class="text-yellow-200/80 text-sm mt-1">{{ installBlockedReason }}</p>
|
|
|
|
|
</div>
|
2026-01-24 22:59:20 +00:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
|
|
|
<!-- Main Content -->
|
|
|
|
|
<div class="lg:col-span-2 space-y-6">
|
|
|
|
|
<!-- Screenshots Gallery -->
|
|
|
|
|
<div class="glass-card p-6">
|
2026-03-11 13:45:59 +00:00
|
|
|
<h2 class="text-2xl font-bold text-white mb-4">{{ t('marketplaceDetails.screenshots') }}</h2>
|
2026-01-24 22:59:20 +00:00
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
|
|
|
<div
|
|
|
|
|
v-for="i in 4"
|
|
|
|
|
:key="i"
|
|
|
|
|
class="aspect-video rounded-xl bg-white/5 border border-white/10 flex items-center justify-center hover:bg-white/10 transition-colors cursor-pointer"
|
|
|
|
|
>
|
|
|
|
|
<svg class="w-16 h-16 text-white/20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
|
|
|
|
</svg>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-03-11 13:45:59 +00:00
|
|
|
<p class="text-white/60 text-sm mt-3 text-center">{{ t('marketplaceDetails.screenshotPlaceholder') }}</p>
|
2026-01-24 22:59:20 +00:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Description -->
|
|
|
|
|
<div class="glass-card p-6">
|
2026-03-11 13:45:59 +00:00
|
|
|
<h2 class="text-2xl font-bold text-white mb-4">{{ t('marketplaceDetails.about', { name: app.title }) }}</h2>
|
2026-01-24 22:59:20 +00:00
|
|
|
<p class="text-white/80 leading-relaxed whitespace-pre-line">
|
|
|
|
|
{{ longDescription }}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Features -->
|
|
|
|
|
<div class="glass-card p-6">
|
2026-03-11 13:45:59 +00:00
|
|
|
<h2 class="text-2xl font-bold text-white mb-4">{{ t('marketplaceDetails.features') }}</h2>
|
2026-01-24 22:59:20 +00:00
|
|
|
<ul class="space-y-3">
|
|
|
|
|
<li
|
|
|
|
|
v-for="(feature, index) in features"
|
|
|
|
|
:key="index"
|
|
|
|
|
class="flex items-start gap-3 text-white/80"
|
|
|
|
|
>
|
|
|
|
|
<svg class="w-6 h-6 text-green-400 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
|
|
|
</svg>
|
|
|
|
|
<span>{{ feature }}</span>
|
|
|
|
|
</li>
|
|
|
|
|
</ul>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Sidebar -->
|
|
|
|
|
<div class="space-y-6">
|
|
|
|
|
<!-- App Info Card -->
|
|
|
|
|
<div class="glass-card p-6">
|
2026-03-11 13:45:59 +00:00
|
|
|
<h3 class="text-lg font-bold text-white mb-4">{{ t('marketplaceDetails.information') }}</h3>
|
2026-01-24 22:59:20 +00:00
|
|
|
<div class="space-y-3">
|
|
|
|
|
<div class="flex items-center justify-between py-2 border-b border-white/10">
|
2026-03-11 13:45:59 +00:00
|
|
|
<span class="text-white/60 text-sm">{{ t('common.version') }}</span>
|
2026-01-24 22:59:20 +00:00
|
|
|
<span class="text-white font-medium">{{ app.version || 'latest' }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div v-if="app.author" class="flex items-center justify-between py-2 border-b border-white/10">
|
2026-03-11 13:45:59 +00:00
|
|
|
<span class="text-white/60 text-sm">{{ t('common.developer') }}</span>
|
2026-01-24 22:59:20 +00:00
|
|
|
<span class="text-white font-medium">{{ app.author }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="flex items-center justify-between py-2 border-b border-white/10">
|
2026-03-11 13:45:59 +00:00
|
|
|
<span class="text-white/60 text-sm">{{ t('common.status') }}</span>
|
|
|
|
|
<span class="text-white font-medium">{{ isInstalled ? t('marketplaceDetails.installed') : t('marketplaceDetails.notInstalled') }}</span>
|
2026-01-24 22:59:20 +00:00
|
|
|
</div>
|
|
|
|
|
<div class="flex items-center justify-between py-2 border-b border-white/10">
|
2026-03-11 13:45:59 +00:00
|
|
|
<span class="text-white/60 text-sm">{{ t('common.category') }}</span>
|
2026-01-24 22:59:20 +00:00
|
|
|
<span class="text-white font-medium capitalize">{{ app.category || 'App' }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div v-if="app.manifestUrl" class="flex items-center justify-between py-2">
|
|
|
|
|
<span class="text-white/60 text-sm">Package</span>
|
|
|
|
|
<span class="text-white font-medium text-xs">.s9pk</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Requirements Card -->
|
|
|
|
|
<div class="glass-card p-6">
|
2026-03-11 13:45:59 +00:00
|
|
|
<h3 class="text-lg font-bold text-white mb-4">{{ t('marketplaceDetails.requirements') }}</h3>
|
2026-01-24 22:59:20 +00:00
|
|
|
<div class="space-y-3">
|
2026-03-09 07:43:12 +00:00
|
|
|
<!-- App Dependencies -->
|
|
|
|
|
<div v-if="dependencies.length > 0" class="space-y-2 mb-4">
|
|
|
|
|
<div
|
|
|
|
|
v-for="dep in dependencies"
|
|
|
|
|
:key="dep.id"
|
|
|
|
|
class="flex items-center gap-3 py-2 border-b border-white/10"
|
|
|
|
|
>
|
|
|
|
|
<!-- Status indicator -->
|
|
|
|
|
<svg v-if="dep.status === 'running'" class="w-5 h-5 text-green-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
|
|
|
</svg>
|
|
|
|
|
<svg v-else-if="dep.status === 'stopped'" class="w-5 h-5 text-yellow-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
|
|
|
|
</svg>
|
|
|
|
|
<svg v-else class="w-5 h-5 text-red-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
|
|
|
</svg>
|
|
|
|
|
<div class="flex-1">
|
|
|
|
|
<p class="text-white/80 font-medium text-sm">{{ dep.title }}</p>
|
|
|
|
|
<p class="text-white/50 text-xs">
|
2026-03-11 13:45:59 +00:00
|
|
|
{{ dep.status === 'running' ? t('marketplaceDetails.depRunning') : dep.status === 'stopped' ? t('marketplaceDetails.depStopped') : t('marketplaceDetails.depNotInstalled') }}
|
2026-03-09 07:43:12 +00:00
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<!-- Install missing dependencies button -->
|
|
|
|
|
<button
|
|
|
|
|
v-if="dependencies.some(d => d.status === 'missing')"
|
|
|
|
|
@click="installDependencies"
|
|
|
|
|
:disabled="installingDeps"
|
|
|
|
|
class="glass-button w-full mt-3 px-4 py-2 rounded-lg text-sm font-medium flex items-center justify-center gap-2"
|
|
|
|
|
>
|
|
|
|
|
<svg v-if="installingDeps" class="animate-spin h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
|
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
|
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
|
|
|
</svg>
|
2026-03-11 13:45:59 +00:00
|
|
|
{{ installingDeps ? t('common.installing') : t('marketplaceDetails.installRequirements') }}
|
2026-03-09 07:43:12 +00:00
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div v-else class="py-2 border-b border-white/10">
|
2026-03-11 13:45:59 +00:00
|
|
|
<p class="text-white/60 text-sm">{{ t('marketplaceDetails.noRequirements') }}</p>
|
2026-03-09 07:43:12 +00:00
|
|
|
</div>
|
2026-01-24 22:59:20 +00:00
|
|
|
<div class="flex items-start gap-3">
|
|
|
|
|
<svg class="w-5 h-5 text-blue-400 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
|
|
|
|
|
</svg>
|
|
|
|
|
<div class="flex-1">
|
2026-03-11 13:45:59 +00:00
|
|
|
<p class="text-white/80 font-medium">{{ t('appDetails.ram') }}</p>
|
|
|
|
|
<p class="text-white/60 text-sm">{{ t('appDetails.ramDesc') }}</p>
|
2026-01-24 22:59:20 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="flex items-start gap-3">
|
|
|
|
|
<svg class="w-5 h-5 text-purple-400 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4" />
|
|
|
|
|
</svg>
|
|
|
|
|
<div class="flex-1">
|
2026-03-11 13:45:59 +00:00
|
|
|
<p class="text-white/80 font-medium">{{ t('appDetails.storage') }}</p>
|
|
|
|
|
<p class="text-white/60 text-sm">{{ t('appDetails.storageDesc') }}</p>
|
2026-01-24 22:59:20 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-02-14 16:44:20 +00:00
|
|
|
<!-- Links Card (no GitHub - repo link removed per product) -->
|
|
|
|
|
<div v-if="app.manifestUrl" class="glass-card p-6">
|
2026-03-11 13:45:59 +00:00
|
|
|
<h3 class="text-lg font-bold text-white mb-4">{{ t('marketplaceDetails.links') }}</h3>
|
2026-01-24 22:59:20 +00:00
|
|
|
<div class="space-y-2">
|
|
|
|
|
<a
|
|
|
|
|
:href="app.manifestUrl"
|
|
|
|
|
target="_blank"
|
|
|
|
|
rel="noopener noreferrer"
|
|
|
|
|
class="flex items-center gap-2 text-blue-400 hover:text-blue-300 transition-colors"
|
|
|
|
|
>
|
|
|
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
|
|
|
</svg>
|
2026-03-11 13:45:59 +00:00
|
|
|
{{ t('marketplaceDetails.downloadPackage') }}
|
2026-01-24 22:59:20 +00:00
|
|
|
</a>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- App Not Found -->
|
2026-03-09 07:43:12 +00:00
|
|
|
<div v-else key="not-found" class="glass-card p-12 text-center">
|
2026-01-24 22:59:20 +00:00
|
|
|
<svg class="w-24 h-24 text-white/20 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
|
|
|
</svg>
|
2026-03-11 13:45:59 +00:00
|
|
|
<h3 class="text-2xl font-semibold text-white mb-2">{{ t('marketplaceDetails.notFoundTitle') }}</h3>
|
|
|
|
|
<p class="text-white/70">{{ t('marketplaceDetails.notFoundMessage') }}</p>
|
2026-01-24 22:59:20 +00:00
|
|
|
</div>
|
2026-03-09 07:43:12 +00:00
|
|
|
</Transition>
|
2026-01-24 22:59:20 +00:00
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2026-03-01 18:07:35 +00:00
|
|
|
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
|
2026-01-24 22:59:20 +00:00
|
|
|
import { useRouter, useRoute } from 'vue-router'
|
2026-03-11 13:45:59 +00:00
|
|
|
import { useI18n } from 'vue-i18n'
|
2026-01-24 22:59:20 +00:00
|
|
|
import { useAppStore } from '../stores/app'
|
|
|
|
|
import { rpcClient } from '../api/rpc-client'
|
2026-03-04 05:23:42 +00:00
|
|
|
import { useMarketplaceApp, type MarketplaceAppInfo } from '../composables/useMarketplaceApp'
|
2026-01-24 22:59:20 +00:00
|
|
|
import { useMobileBackButton } from '../composables/useMobileBackButton'
|
2026-03-12 00:19:30 +00:00
|
|
|
import { useAppLauncherStore } from '../stores/appLauncher'
|
2026-05-05 11:29:18 -04:00
|
|
|
import { useToast } from '../composables/useToast'
|
2026-05-13 15:09:22 -04:00
|
|
|
import { handleImageError } from './apps/appsConfig'
|
2026-01-24 22:59:20 +00:00
|
|
|
|
2026-03-11 13:45:59 +00:00
|
|
|
const { t } = useI18n()
|
2026-01-24 22:59:20 +00:00
|
|
|
const { bottomPosition } = useMobileBackButton()
|
2026-05-05 11:29:18 -04:00
|
|
|
const toast = useToast()
|
2026-01-24 22:59:20 +00:00
|
|
|
|
|
|
|
|
const router = useRouter()
|
|
|
|
|
const route = useRoute()
|
|
|
|
|
const store = useAppStore()
|
|
|
|
|
const { getCurrentApp } = useMarketplaceApp()
|
|
|
|
|
|
2026-03-04 05:23:42 +00:00
|
|
|
const app = ref<MarketplaceAppInfo | null>(null)
|
2026-01-24 22:59:20 +00:00
|
|
|
const installing = ref(false)
|
2026-03-09 07:43:12 +00:00
|
|
|
const installingDeps = ref(false)
|
2026-01-24 22:59:20 +00:00
|
|
|
const installError = ref<string | null>(null)
|
|
|
|
|
const loading = ref(true)
|
2026-05-05 11:29:18 -04:00
|
|
|
const bitcoinPruned = ref(false)
|
|
|
|
|
const electrumxArchiveWarning = 'You need a full archival bitcoin node before downloading ElectrumX'
|
2026-01-24 22:59:20 +00:00
|
|
|
|
|
|
|
|
const appId = computed(() => route.params.id as string)
|
|
|
|
|
|
2026-03-12 00:19:30 +00:00
|
|
|
// Web-only apps (no container, just a URL) — always treated as "installed"
|
|
|
|
|
const isWebOnly = computed(() => {
|
|
|
|
|
return !!(app.value?.webUrl && !app.value?.dockerImage)
|
|
|
|
|
})
|
|
|
|
|
|
2026-01-24 22:59:20 +00:00
|
|
|
// Check if app is already installed
|
|
|
|
|
const isInstalled = computed(() => {
|
2026-03-12 00:19:30 +00:00
|
|
|
if (isWebOnly.value) return true
|
2026-01-24 22:59:20 +00:00
|
|
|
return !!store.packages[appId.value]
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Extract descriptions with safety checks
|
|
|
|
|
const shortDescription = computed(() => {
|
|
|
|
|
try {
|
|
|
|
|
if (!app.value) return ''
|
|
|
|
|
const desc = app.value.description
|
|
|
|
|
if (typeof desc === 'object' && desc) {
|
|
|
|
|
return desc.short || desc.long || ''
|
|
|
|
|
}
|
|
|
|
|
return desc || ''
|
|
|
|
|
} catch (e) {
|
2026-03-11 13:45:59 +00:00
|
|
|
if (import.meta.env.DEV) console.error('[MarketplaceAppDetails] Error in shortDescription:', e)
|
2026-01-24 22:59:20 +00:00
|
|
|
return ''
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const longDescription = computed(() => {
|
|
|
|
|
try {
|
|
|
|
|
if (!app.value) return ''
|
|
|
|
|
const desc = app.value.description
|
|
|
|
|
if (typeof desc === 'object' && desc) {
|
|
|
|
|
return desc.long || desc.short || ''
|
|
|
|
|
}
|
|
|
|
|
return desc || ''
|
|
|
|
|
} catch (e) {
|
2026-03-11 13:45:59 +00:00
|
|
|
if (import.meta.env.DEV) console.error('[MarketplaceAppDetails] Error in longDescription:', e)
|
2026-01-24 22:59:20 +00:00
|
|
|
return ''
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Placeholder features
|
|
|
|
|
const features = computed(() => {
|
|
|
|
|
return [
|
|
|
|
|
'Self-hosted and privacy-focused',
|
|
|
|
|
'Easy installation and updates',
|
|
|
|
|
'Automatic backups',
|
|
|
|
|
'Secure by default',
|
|
|
|
|
'Open source'
|
|
|
|
|
]
|
|
|
|
|
})
|
|
|
|
|
|
2026-03-09 07:43:12 +00:00
|
|
|
/** App dependency definitions */
|
chore: release v1.7.45-alpha
Resilience-validated release. Three full sweeps of the new resilience
harness against .228 confirm no shipstoppers.
Big user-visible:
- Bitcoin RPC auth durably correct via host-rendered nginx.conf bind-mount,
replaces fragile post-start exec that failed under restricted-cap rootless
podman ("crun: write cgroup.procs: Permission denied")
- Multi-container stack installs (indeedhub, immich, btcpay, mempool) now
emit phase events at every boundary so the progress bar advances
- Apps no longer vanish from the dashboard mid-install (absent-scanner skips
packages in transitional states)
- Indeedhub fresh installs work end-to-end (was 8500+ restart loop): five
missing env vars (DATABASE_PORT, QUEUE_HOST, QUEUE_PORT,
S3_PRIVATE_BUCKET_NAME, AES_MASTER_SECRET) added to install code
- Tailscale install fixed: --entrypoint string was being passed as a single
shell-line arg; switched to custom_args array
- Catalog cleaned of broken entries (dwn, endurain, ollama removed; nextcloud
restored on docker.io)
- Bitcoin Core update path uses correct image (was looking for nonexistent
lfg2025/bitcoin:28.4)
- ISO installs now allocate swap on the encrypted data partition
Infra:
- New resilience harness (scripts/resilience/) — black-box state-machine
tester, every app × every transition. Run before each release.
Sweep #3 final: PASS 107 / FAIL 12 / SKIP 14. The 12 fails are 1 cosmetic
(homeassistant trusted_hosts), 8 harness/timing false-positives, and 3
non-shipstopper tracked items. Down from 23 in baseline sweep #1.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 12:31:45 -04:00
|
|
|
const R = '146.59.87.168:3000/lfg2025'
|
2026-03-09 07:43:12 +00:00
|
|
|
const APP_DEPENDENCIES: Record<string, { id: string; title: string; dockerImage: string }[]> = {
|
2026-03-30 18:34:06 +01:00
|
|
|
'electrumx': [{ id: 'bitcoin-knots', title: 'Bitcoin Knots', dockerImage: `${R}/bitcoin-knots:latest` }],
|
|
|
|
|
'lnd': [{ id: 'bitcoin-knots', title: 'Bitcoin Knots', dockerImage: `${R}/bitcoin-knots:latest` }],
|
|
|
|
|
'btcpay-server': [{ id: 'bitcoin-knots', title: 'Bitcoin Knots', dockerImage: `${R}/bitcoin-knots:latest` }],
|
2026-03-09 07:43:12 +00:00
|
|
|
'mempool': [
|
2026-03-30 18:34:06 +01:00
|
|
|
{ id: 'bitcoin-knots', title: 'Bitcoin Knots', dockerImage: `${R}/bitcoin-knots:latest` },
|
|
|
|
|
{ id: 'electrumx', title: 'ElectrumX', dockerImage: `${R}/electrumx:v1.18.0` },
|
2026-03-09 07:43:12 +00:00
|
|
|
],
|
2026-03-30 18:34:06 +01:00
|
|
|
'fedimint': [{ id: 'bitcoin-knots', title: 'Bitcoin Knots', dockerImage: `${R}/bitcoin-knots:latest` }],
|
2026-03-09 07:43:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Check dependency status against installed packages */
|
|
|
|
|
const dependencies = computed(() => {
|
|
|
|
|
if (!app.value) return []
|
|
|
|
|
const deps = APP_DEPENDENCIES[app.value.id]
|
|
|
|
|
if (!deps) return []
|
|
|
|
|
return deps.map(dep => {
|
|
|
|
|
const pkg = store.packages[dep.id]
|
|
|
|
|
let status: 'running' | 'stopped' | 'missing' = 'missing'
|
|
|
|
|
if (pkg) {
|
|
|
|
|
status = pkg.state === 'running' ? 'running' : 'stopped'
|
|
|
|
|
}
|
|
|
|
|
return { ...dep, status }
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
2026-05-05 11:29:18 -04:00
|
|
|
const installBlockedReason = computed(() => {
|
|
|
|
|
const id = app.value?.id
|
|
|
|
|
if (!bitcoinPruned.value || !id) return ''
|
|
|
|
|
if (id !== 'electrumx' && id !== 'electrs' && id !== 'mempool-electrs') return ''
|
|
|
|
|
return electrumxArchiveWarning
|
|
|
|
|
})
|
|
|
|
|
|
2026-03-01 18:07:35 +00:00
|
|
|
let pendingRedirect: ReturnType<typeof setTimeout> | null = null
|
|
|
|
|
|
2026-01-24 22:59:20 +00:00
|
|
|
onMounted(() => {
|
2026-03-01 18:07:35 +00:00
|
|
|
if (import.meta.env.DEV) console.log('[MarketplaceAppDetails] Loading app ID:', appId.value)
|
2026-01-24 22:59:20 +00:00
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const loadedApp = getCurrentApp()
|
|
|
|
|
|
|
|
|
|
if (loadedApp && loadedApp.id === appId.value) {
|
|
|
|
|
app.value = loadedApp
|
|
|
|
|
loading.value = false
|
|
|
|
|
} else {
|
|
|
|
|
loading.value = false
|
2026-03-01 18:07:35 +00:00
|
|
|
pendingRedirect = setTimeout(() => {
|
|
|
|
|
router.push('/dashboard/marketplace').catch(() => {})
|
2026-01-24 22:59:20 +00:00
|
|
|
}, 500)
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
2026-03-11 13:45:59 +00:00
|
|
|
if (import.meta.env.DEV) console.error('[MarketplaceAppDetails] Error loading app data:', e)
|
2026-01-24 22:59:20 +00:00
|
|
|
loading.value = false
|
2026-03-01 18:07:35 +00:00
|
|
|
pendingRedirect = setTimeout(() => {
|
|
|
|
|
router.push('/dashboard/marketplace').catch(() => {})
|
2026-01-24 22:59:20 +00:00
|
|
|
}, 500)
|
|
|
|
|
}
|
2026-05-05 11:29:18 -04:00
|
|
|
loadBitcoinPruneStatus()
|
2026-01-24 22:59:20 +00:00
|
|
|
})
|
|
|
|
|
|
2026-05-05 11:29:18 -04:00
|
|
|
async function loadBitcoinPruneStatus() {
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch('/bitcoin-status', { credentials: 'include', signal: AbortSignal.timeout(8000) })
|
|
|
|
|
if (!res.ok) return
|
|
|
|
|
const status = await res.json()
|
|
|
|
|
bitcoinPruned.value = status?.blockchain_info?.pruned === true
|
|
|
|
|
} catch (e) {
|
|
|
|
|
if (import.meta.env.DEV) console.warn('[MarketplaceAppDetails] Bitcoin prune status unavailable:', e)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-01 18:07:35 +00:00
|
|
|
onBeforeUnmount(() => {
|
|
|
|
|
if (pendingRedirect) { clearTimeout(pendingRedirect); pendingRedirect = null }
|
|
|
|
|
})
|
|
|
|
|
|
2026-01-24 22:59:20 +00:00
|
|
|
function goBack() {
|
feat: add Discover page — cypherpunk app store with sovereignty messaging
- New Discover.vue with hero banner, featured sovereignty stack apps,
principle cards, manifesto footer, and full app grid
- Featured apps (Bitcoin Knots, LND, BTCPay, Vaultwarden) with
expanded privacy/sovereignty descriptions
- Discover is first tab in categories bar on App Store pages
- Smart back navigation: detail pages return to Discover when navigated from there
- Category clicks from Discover navigate to Marketplace with category pre-selected
- Cypherpunk aesthetic: terminal tags, scanline overlays, gradient accents,
animated Bitcoin orange headings
- Global CSS classes: discover-hero, discover-terminal-tag, discover-featured-card,
discover-principle-card, discover-manifesto
- Route added: /dashboard/discover
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 15:14:12 +00:00
|
|
|
if (route.query.from === 'discover') {
|
|
|
|
|
router.push('/dashboard/discover').catch(() => {})
|
|
|
|
|
} else {
|
|
|
|
|
router.push('/dashboard/marketplace').catch(() => {})
|
|
|
|
|
}
|
2026-01-24 22:59:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function goToInstalledApp() {
|
2026-03-12 00:19:30 +00:00
|
|
|
// Web-only apps: launch directly via appLauncher
|
|
|
|
|
if (isWebOnly.value && app.value?.webUrl) {
|
|
|
|
|
useAppLauncherStore().open({
|
|
|
|
|
url: app.value.webUrl,
|
|
|
|
|
title: app.value.title || appId.value,
|
|
|
|
|
})
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-01-24 22:59:20 +00:00
|
|
|
router.push({
|
|
|
|
|
path: `/dashboard/apps/${appId.value}`,
|
|
|
|
|
query: { from: 'marketplace' }
|
2026-03-01 18:07:35 +00:00
|
|
|
}).catch(() => {})
|
2026-01-24 22:59:20 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-09 07:43:12 +00:00
|
|
|
async function installDependencies() {
|
|
|
|
|
if (installingDeps.value) return
|
|
|
|
|
const missingDeps = dependencies.value.filter(d => d.status === 'missing')
|
|
|
|
|
if (!missingDeps.length) return
|
2026-05-05 11:29:18 -04:00
|
|
|
if (bitcoinPruned.value && missingDeps.some(d => d.id === 'electrumx' || d.id === 'electrs' || d.id === 'mempool-electrs')) {
|
|
|
|
|
installError.value = electrumxArchiveWarning
|
|
|
|
|
toast.error(electrumxArchiveWarning)
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-03-09 07:43:12 +00:00
|
|
|
|
|
|
|
|
installingDeps.value = true
|
|
|
|
|
installError.value = null
|
|
|
|
|
|
|
|
|
|
try {
|
2026-03-16 12:58:35 +00:00
|
|
|
// Install dependencies sequentially (order matters: bitcoin before electrumx)
|
2026-03-09 07:43:12 +00:00
|
|
|
for (const dep of missingDeps) {
|
|
|
|
|
await rpcClient.call({
|
|
|
|
|
method: 'package.install',
|
|
|
|
|
params: {
|
|
|
|
|
id: dep.id,
|
|
|
|
|
dockerImage: dep.dockerImage,
|
|
|
|
|
},
|
2026-04-23 06:58:02 -04:00
|
|
|
timeout: 15000,
|
2026-03-09 07:43:12 +00:00
|
|
|
})
|
|
|
|
|
// Wait for package to register before installing next
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 2000))
|
|
|
|
|
}
|
|
|
|
|
} catch (err: unknown) {
|
2026-03-11 13:45:59 +00:00
|
|
|
installError.value = err instanceof Error ? err.message : t('marketplaceDetails.installFailed')
|
|
|
|
|
if (import.meta.env.DEV) console.error('[MarketplaceAppDetails] Failed to install dependencies:', err)
|
2026-03-09 07:43:12 +00:00
|
|
|
} finally {
|
|
|
|
|
installingDeps.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-24 22:59:20 +00:00
|
|
|
async function installApp() {
|
2026-03-09 07:43:12 +00:00
|
|
|
if (installing.value || !app.value) return
|
2026-05-05 11:29:18 -04:00
|
|
|
if (installBlockedReason.value) {
|
|
|
|
|
installError.value = installBlockedReason.value
|
|
|
|
|
toast.error(installBlockedReason.value)
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-03-09 07:43:12 +00:00
|
|
|
if (!app.value.manifestUrl && !app.value.dockerImage) {
|
2026-03-11 13:45:59 +00:00
|
|
|
if (import.meta.env.DEV) console.warn('[MarketplaceAppDetails] Cannot install - no manifestUrl or dockerImage:', app.value)
|
2026-01-24 22:59:20 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
installing.value = true
|
|
|
|
|
installError.value = null
|
|
|
|
|
|
|
|
|
|
try {
|
2026-03-09 07:43:12 +00:00
|
|
|
if (app.value.dockerImage) {
|
|
|
|
|
// Docker-based app installation
|
2026-05-13 15:09:22 -04:00
|
|
|
const installParams: Record<string, unknown> = {
|
|
|
|
|
id: app.value.id,
|
|
|
|
|
dockerImage: app.value.dockerImage,
|
|
|
|
|
version: app.value.version,
|
|
|
|
|
}
|
|
|
|
|
if (app.value.containerConfig) installParams.containerConfig = app.value.containerConfig
|
2026-01-24 22:59:20 +00:00
|
|
|
await rpcClient.call({
|
|
|
|
|
method: 'package.install',
|
2026-05-13 15:09:22 -04:00
|
|
|
params: installParams,
|
2026-05-19 14:29:20 -04:00
|
|
|
timeout: 600000,
|
2026-01-24 22:59:20 +00:00
|
|
|
})
|
|
|
|
|
} else {
|
2026-03-09 07:43:12 +00:00
|
|
|
// Package-based installation
|
|
|
|
|
const installUrl = app.value.url || app.value.manifestUrl
|
2026-01-24 22:59:20 +00:00
|
|
|
await rpcClient.call({
|
|
|
|
|
method: 'package.install',
|
|
|
|
|
params: {
|
|
|
|
|
id: app.value.id,
|
|
|
|
|
url: installUrl,
|
2026-03-09 07:43:12 +00:00
|
|
|
version: app.value.version,
|
2026-01-24 22:59:20 +00:00
|
|
|
},
|
2026-05-19 14:29:20 -04:00
|
|
|
timeout: 600000,
|
2026-01-24 22:59:20 +00:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Wait a moment for the package to be registered
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 1000))
|
|
|
|
|
|
2026-03-01 18:07:35 +00:00
|
|
|
router.push(`/dashboard/apps/${appId.value}`).catch(() => {})
|
2026-03-04 05:23:42 +00:00
|
|
|
} catch (err: unknown) {
|
2026-03-11 13:45:59 +00:00
|
|
|
installError.value = err instanceof Error ? err.message : t('marketplaceDetails.installFailed')
|
|
|
|
|
if (import.meta.env.DEV) console.error('[MarketplaceAppDetails] Failed to install app:', err)
|
2026-01-24 22:59:20 +00:00
|
|
|
} finally {
|
|
|
|
|
installing.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</script>
|