refactor: split Marketplace, Server, Home, AppDetails views; minor frontend quality fixes
- F29-F32: Split 4 views (Marketplace 1293→505, Server 1132→486, Home 1059→394, AppDetails 1036→386) - F20: Add aria-current="page" to Dashboard nav links - F21: Add 150ms search debounce in Marketplace and Apps views - F22: Reduce backdrop-filter blur to 8px on mobile for GPU performance - F23: Track and clear WebSocket connect check interval in all paths Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
afd7405b1a
commit
69e25410b0
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<div class="lg:col-span-2 space-y-6">
|
||||
<!-- Screenshots Gallery -->
|
||||
<div class="glass-card p-6">
|
||||
<h2 class="text-2xl font-bold text-white mb-4">{{ t('appDetails.screenshots') }}</h2>
|
||||
<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>
|
||||
<p class="text-white/60 text-sm mt-3 text-center">{{ t('appDetails.screenshotPlaceholder') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Bitcoin Sync Warning (for dependent apps) -->
|
||||
<div v-if="needsBitcoinSync && !bitcoinSynced" class="glass-card p-5 border border-orange-500/30">
|
||||
<div class="flex items-center gap-3 mb-3">
|
||||
<svg class="w-6 h-6 text-orange-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-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z" />
|
||||
</svg>
|
||||
<div class="flex-1">
|
||||
<p class="text-orange-300 font-semibold text-sm">Bitcoin is syncing</p>
|
||||
<p class="text-white/60 text-xs mt-0.5">
|
||||
Some features may be unavailable until Bitcoin finishes syncing.
|
||||
Wallet connections and block data require a fully synced node.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full h-2 bg-white/10 rounded-full overflow-hidden">
|
||||
<div
|
||||
class="h-full rounded-full bg-orange-400 transition-all duration-500"
|
||||
:style="{ width: Math.min(bitcoinSyncPercent, 100) + '%' }"
|
||||
></div>
|
||||
</div>
|
||||
<p class="text-xs text-white/40 mt-1.5">
|
||||
{{ bitcoinSyncPercent.toFixed(1) }}% synced — Block {{ bitcoinBlockHeight.toLocaleString() }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div class="glass-card p-6">
|
||||
<h2 class="text-2xl font-bold text-white mb-4">{{ t('appDetails.about', { name: pkg.manifest.title }) }}</h2>
|
||||
<p class="text-white/80 leading-relaxed whitespace-pre-line">
|
||||
{{ pkg.manifest.description.long }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Features (if available) -->
|
||||
<div v-if="features.length > 0" class="glass-card p-6">
|
||||
<h2 class="text-2xl font-bold text-white mb-4">{{ t('appDetails.features') }}</h2>
|
||||
<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>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
defineProps<{
|
||||
pkg: Record<string, any>
|
||||
features: string[]
|
||||
needsBitcoinSync: boolean
|
||||
bitcoinSynced: boolean
|
||||
bitcoinSyncPercent: number
|
||||
bitcoinBlockHeight: number
|
||||
}>()
|
||||
</script>
|
||||
@@ -0,0 +1,212 @@
|
||||
<template>
|
||||
<div class="glass-card p-6 mb-6">
|
||||
<!-- Desktop: Single Row Layout -->
|
||||
<div class="hidden md:flex items-center gap-6">
|
||||
<img
|
||||
: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"
|
||||
/>
|
||||
|
||||
<div class="flex-1 min-w-0">
|
||||
<h1 class="text-2xl font-bold text-white mb-1">{{ pkg.manifest.title }}</h1>
|
||||
<p class="text-white/70 text-sm mb-2">{{ pkg.manifest.description.short }}</p>
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class="inline-flex items-center px-2.5 py-1 rounded-lg text-xs font-medium"
|
||||
:class="getStatusClass(pkg.state, pkg.health)"
|
||||
>
|
||||
<span class="w-1.5 h-1.5 rounded-full mr-1.5" :class="getStatusDotClass(pkg.state, pkg.health)"></span>
|
||||
{{ getStatusLabel(pkg.state, pkg.health) }}
|
||||
</span>
|
||||
<span class="text-white/50 text-xs">v{{ pkg.manifest.version }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex items-center gap-2 flex-shrink-0">
|
||||
<button
|
||||
v-if="packageKey === 'lnd'"
|
||||
@click="$emit('channels')"
|
||||
class="glass-button glass-button-sm px-4 py-2.5 rounded-lg text-sm font-medium flex items-center gap-2"
|
||||
>
|
||||
<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="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
{{ t('appDetails.channels') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="canLaunch"
|
||||
@click="$emit('launch')"
|
||||
class="glass-button glass-button-sm px-6 py-2.5 rounded-lg text-sm font-semibold flex items-center gap-2"
|
||||
>
|
||||
<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>
|
||||
{{ t('common.launch') }}
|
||||
</button>
|
||||
<template v-if="!isWebOnly">
|
||||
<button
|
||||
v-if="pkg.state === 'stopped' || pkg.state === 'exited'"
|
||||
@click="$emit('start')"
|
||||
class="px-4 py-2.5 glass-button rounded-lg text-sm font-medium transition-colors flex items-center gap-2"
|
||||
:class="pkg.state === 'exited' ? 'glass-button-danger' : 'glass-button-success'"
|
||||
>
|
||||
<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="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
|
||||
</svg>
|
||||
{{ pkg.state === 'exited' ? 'Restart' : t('common.start') }}
|
||||
</button>
|
||||
<button
|
||||
@click="$emit('restart')"
|
||||
class="px-4 py-2.5 glass-button rounded-lg text-sm font-medium hover:bg-white/15 transition-colors flex items-center gap-2"
|
||||
>
|
||||
<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="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
{{ t('common.restart') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="pkg.state === 'running'"
|
||||
@click="$emit('stop')"
|
||||
class="px-4 py-2.5 bg-yellow-500/20 border border-yellow-500/40 rounded-lg text-yellow-200 text-sm font-medium hover:bg-yellow-500/30 transition-colors flex items-center gap-2"
|
||||
>
|
||||
<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="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 10a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z" />
|
||||
</svg>
|
||||
{{ t('common.stop') }}
|
||||
</button>
|
||||
<button
|
||||
@click="$emit('uninstall')"
|
||||
class="px-4 py-2.5 glass-button glass-button-danger rounded-lg text-sm font-medium transition-colors flex items-center gap-2"
|
||||
>
|
||||
<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="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
{{ t('common.uninstall') }}
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile: Two Column Grid Layout -->
|
||||
<div class="md:hidden">
|
||||
<div class="flex items-start gap-4 mb-4">
|
||||
<img
|
||||
: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"
|
||||
/>
|
||||
|
||||
<div class="flex-1 min-w-0">
|
||||
<h1 class="text-xl font-bold text-white mb-1">{{ pkg.manifest.title }}</h1>
|
||||
<p class="text-white/70 text-xs mb-2 line-clamp-2">{{ pkg.manifest.description.short }}</p>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span
|
||||
class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium"
|
||||
:class="getStatusClass(pkg.state, pkg.health)"
|
||||
>
|
||||
<span class="w-1.5 h-1.5 rounded-full mr-1" :class="getStatusDotClass(pkg.state, pkg.health)"></span>
|
||||
{{ getStatusLabel(pkg.state, pkg.health) }}
|
||||
</span>
|
||||
<span class="text-white/50 text-xs">v{{ pkg.manifest.version }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
v-if="!isWebOnly"
|
||||
@click="$emit('uninstall')"
|
||||
class="flex-shrink-0 w-10 h-10 rounded-lg glass-button glass-button-danger transition-colors flex items-center justify-center"
|
||||
:title="t('common.uninstall')"
|
||||
>
|
||||
<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="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons (Auto Grid) -->
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<button
|
||||
v-if="canLaunch"
|
||||
@click="$emit('launch')"
|
||||
:class="isWebOnly ? 'col-span-2' : ''"
|
||||
class="glass-button glass-button-sm px-4 py-2.5 rounded-lg text-sm font-semibold flex items-center justify-center gap-2"
|
||||
>
|
||||
<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>
|
||||
{{ t('common.launch') }}
|
||||
</button>
|
||||
<template v-if="!isWebOnly">
|
||||
<button
|
||||
v-if="pkg.state === 'stopped' || pkg.state === 'exited'"
|
||||
@click="$emit('start')"
|
||||
class="px-4 py-2.5 glass-button rounded-lg text-sm font-medium transition-colors flex items-center justify-center gap-2"
|
||||
:class="pkg.state === 'exited' ? 'glass-button-danger' : 'glass-button-success'"
|
||||
>
|
||||
<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="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
|
||||
</svg>
|
||||
{{ pkg.state === 'exited' ? 'Restart' : t('common.start') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="pkg.state === 'running'"
|
||||
@click="$emit('stop')"
|
||||
class="px-4 py-2.5 bg-yellow-500/20 border border-yellow-500/40 rounded-lg text-yellow-200 text-sm font-medium hover:bg-yellow-500/30 transition-colors flex items-center justify-center gap-2"
|
||||
>
|
||||
<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="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 10a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z" />
|
||||
</svg>
|
||||
{{ t('common.stop') }}
|
||||
</button>
|
||||
<button
|
||||
@click="$emit('restart')"
|
||||
:class="[canLaunch && (pkg.state === 'stopped' || pkg.state === 'exited' || pkg.state === 'running') ? 'col-span-2' : '']"
|
||||
class="px-4 py-2.5 glass-button rounded-lg text-sm font-medium hover:bg-white/15 transition-colors flex items-center justify-center gap-2"
|
||||
>
|
||||
<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="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
{{ t('common.restart') }}
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { getStatusClass, getStatusDotClass, getStatusLabel } from './appDetailsData'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
defineProps<{
|
||||
pkg: Record<string, any>
|
||||
appId: string
|
||||
packageKey: string
|
||||
canLaunch: boolean
|
||||
isWebOnly: boolean
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
launch: []
|
||||
start: []
|
||||
stop: []
|
||||
restart: []
|
||||
uninstall: []
|
||||
channels: []
|
||||
}>()
|
||||
|
||||
function handleImageError(e: Event) {
|
||||
const target = e.target as HTMLImageElement
|
||||
if (!target.src.includes('data:image') && !target.src.includes('logo-archipelago')) {
|
||||
target.src = '/assets/img/logo-archipelago.svg'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,163 @@
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<!-- App Info Card -->
|
||||
<div class="glass-card p-6">
|
||||
<h3 class="text-lg font-bold text-white mb-4">{{ t('appDetails.information') }}</h3>
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center justify-between py-2 border-b border-white/10">
|
||||
<span class="text-white/60 text-sm">{{ t('common.version') }}</span>
|
||||
<span class="text-white font-medium">{{ pkg.manifest.version }}</span>
|
||||
</div>
|
||||
<div v-if="pkg.manifest.author" class="flex items-center justify-between py-2 border-b border-white/10">
|
||||
<span class="text-white/60 text-sm">{{ t('common.developer') }}</span>
|
||||
<span class="text-white font-medium">{{ pkg.manifest.author }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between py-2 border-b border-white/10">
|
||||
<span class="text-white/60 text-sm">{{ t('common.status') }}</span>
|
||||
<span class="text-white font-medium capitalize">{{ pkg.state }}</span>
|
||||
</div>
|
||||
<div v-if="pkg.manifest.license" class="flex items-center justify-between py-2 border-b border-white/10">
|
||||
<span class="text-white/60 text-sm">{{ t('common.license') }}</span>
|
||||
<span class="text-white font-medium">{{ pkg.manifest.license }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between py-2">
|
||||
<span class="text-white/60 text-sm">{{ t('common.category') }}</span>
|
||||
<span class="text-white font-medium">App</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fedimint Services Card -->
|
||||
<div v-if="packageKey === 'fedimint'" class="glass-card p-6">
|
||||
<h3 class="text-lg font-bold text-white mb-4">{{ t('appDetails.services') }}</h3>
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center gap-3 py-2 border-b border-white/10">
|
||||
<span class="w-2 h-2 rounded-full" :class="pkg.state === 'running' ? 'bg-green-400' : 'bg-yellow-400'"></span>
|
||||
<div class="flex-1">
|
||||
<p class="text-white/80 font-medium text-sm">{{ t('appDetails.guardian') }}</p>
|
||||
<p class="text-white/50 text-xs capitalize">{{ pkg.state }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 py-2">
|
||||
<span class="w-2 h-2 rounded-full" :class="gatewayState === 'running' ? 'bg-green-400' : gatewayState === 'stopped' ? 'bg-yellow-400' : 'bg-red-400'"></span>
|
||||
<div class="flex-1">
|
||||
<p class="text-white/80 font-medium text-sm">{{ t('appDetails.gateway') }}</p>
|
||||
<p class="text-white/50 text-xs capitalize">{{ gatewayState }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Access (LAN + Tor) Card -->
|
||||
<div v-if="interfaceAddresses" class="glass-card p-6">
|
||||
<h3 class="text-lg font-bold text-white mb-4">{{ t('appDetails.access') }}</h3>
|
||||
<div class="space-y-3">
|
||||
<div v-if="interfaceAddresses['lan-address']" class="flex items-start gap-3">
|
||||
<svg class="w-5 h-5 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="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" />
|
||||
</svg>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-white/80 font-medium">{{ t('appDetails.lan') }}</p>
|
||||
<a
|
||||
:href="lanUrl"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-blue-400 hover:text-blue-300 text-sm break-all"
|
||||
>
|
||||
{{ interfaceAddresses['lan-address'] }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="showTorAddress" class="flex items-start gap-3">
|
||||
<svg class="w-5 h-5 text-amber-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="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />
|
||||
</svg>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-white/80 font-medium">{{ t('appDetails.tor') }}</p>
|
||||
<span class="text-amber-300/90 text-sm font-mono break-all">{{ torUrl }}</span>
|
||||
<p class="text-white/50 text-xs mt-1">{{ t('appDetails.requiresTor') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Requirements Card (hidden for web-only apps) -->
|
||||
<div v-if="!isWebOnly" class="glass-card p-6">
|
||||
<h3 class="text-lg font-bold text-white mb-4">{{ t('appDetails.requirements') }}</h3>
|
||||
<div class="space-y-3">
|
||||
<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">
|
||||
<p class="text-white/80 font-medium">{{ t('appDetails.ram') }}</p>
|
||||
<p class="text-white/60 text-sm">{{ t('appDetails.ramDesc') }}</p>
|
||||
</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">
|
||||
<p class="text-white/80 font-medium">{{ t('appDetails.storage') }}</p>
|
||||
<p class="text-white/60 text-sm">{{ t('appDetails.storageDesc') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Links Card -->
|
||||
<div class="glass-card p-6">
|
||||
<h3 class="text-lg font-bold text-white mb-4">{{ t('appDetails.links') }}</h3>
|
||||
<div class="space-y-2">
|
||||
<a
|
||||
v-if="pkg.manifest.website"
|
||||
:href="pkg.manifest.website"
|
||||
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="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" />
|
||||
</svg>
|
||||
{{ t('appDetails.website') }}
|
||||
</a>
|
||||
<a
|
||||
href="#"
|
||||
class="flex items-center gap-2 text-blue-400 hover:text-blue-300 transition-colors"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.840 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
|
||||
</svg>
|
||||
{{ t('appDetails.sourceCode') }}
|
||||
</a>
|
||||
<a
|
||||
href="#"
|
||||
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="M9 12h6m-6 4h6m2 5H7a2 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>
|
||||
{{ t('appDetails.documentation') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
defineProps<{
|
||||
pkg: Record<string, any>
|
||||
packageKey: string
|
||||
isWebOnly: boolean
|
||||
gatewayState: string
|
||||
interfaceAddresses: Record<string, string> | null
|
||||
lanUrl: string
|
||||
torUrl: string
|
||||
showTorAddress: boolean
|
||||
}>()
|
||||
</script>
|
||||
@@ -0,0 +1,158 @@
|
||||
/**
|
||||
* AppDetails data: URL maps, route-to-package mappings, aliases, and status helpers.
|
||||
* Extracted from AppDetails.vue to keep the view under 500 lines.
|
||||
*/
|
||||
import { PackageState } from '@/types/api'
|
||||
|
||||
/** Web-only app detection (no container -- external websites) */
|
||||
export const WEB_ONLY_APP_URLS: Record<string, string> = {
|
||||
'indeedhub': `${window.location.protocol}//${window.location.hostname}:7777`,
|
||||
'botfights': 'https://botfights.net',
|
||||
'nwnn': 'https://nwnn.l484.com',
|
||||
'484-kitchen': 'https://484.kitchen',
|
||||
'call-the-operator': 'https://cta.tx1138.com',
|
||||
'arch-presentation': 'https://present.l484.com',
|
||||
'syntropy-institute': 'https://syntropy.institute',
|
||||
't-zero': 'https://teeminuszero.net',
|
||||
}
|
||||
|
||||
/** Map route/marketplace app IDs to backend package keys (container names). */
|
||||
export const ROUTE_TO_PACKAGE_KEY: Record<string, string> = {
|
||||
mempool: 'mempool-web',
|
||||
'mempool-electrs': 'mempool-electrs',
|
||||
electrs: 'mempool-electrs',
|
||||
btcpay: 'btcpay-server',
|
||||
'btcpay-server': 'btcpay-server',
|
||||
fedimint: 'fedimint',
|
||||
'fedimint-gateway': 'fedimint-gateway',
|
||||
lnd: 'lnd',
|
||||
'lnd-ui': 'lnd',
|
||||
bitcoin: 'bitcoin-knots',
|
||||
'bitcoin-knots': 'bitcoin-knots',
|
||||
homeassistant: 'homeassistant',
|
||||
'home-assistant': 'homeassistant',
|
||||
grafana: 'grafana',
|
||||
searxng: 'searxng',
|
||||
ollama: 'ollama',
|
||||
onlyoffice: 'onlyoffice',
|
||||
penpot: 'penpot',
|
||||
nextcloud: 'nextcloud',
|
||||
vaultwarden: 'vaultwarden',
|
||||
jellyfin: 'jellyfin',
|
||||
photoprism: 'photoprism',
|
||||
immich: 'immich',
|
||||
filebrowser: 'filebrowser',
|
||||
'nginx-proxy-manager': 'nginx-proxy-manager',
|
||||
portainer: 'portainer',
|
||||
'uptime-kuma': 'uptime-kuma',
|
||||
tailscale: 'tailscale',
|
||||
}
|
||||
|
||||
/** Backend may register under variant container names */
|
||||
export const PACKAGE_ALIASES: Record<string, string[]> = {
|
||||
immich: ['immich_server', 'immich-server'],
|
||||
nextcloud: ['nextcloud-aio', 'nextcloud-server'],
|
||||
}
|
||||
|
||||
export function resolvePackageKey(routeId: string): string {
|
||||
return ROUTE_TO_PACKAGE_KEY[routeId] ?? routeId
|
||||
}
|
||||
|
||||
/** Apps that depend on Bitcoin being synced */
|
||||
export const BITCOIN_DEPENDENT_APPS = ['lnd', 'electrumx', 'electrs', 'mempool-electrs', 'btcpay-server', 'btcpayserver']
|
||||
|
||||
/** App launch URLs for dev and prod environments */
|
||||
export const APP_URLS: Record<string, { dev: string; prod: string }> = {
|
||||
'lorabell': { dev: 'http://192.168.1.166', prod: 'http://192.168.1.166' },
|
||||
'atob': { dev: 'http://localhost:8102', prod: 'https://app.atobitcoin.io' },
|
||||
'k484': { dev: 'http://localhost:8103', prod: 'http://localhost:8103' },
|
||||
'indeedhub': { dev: 'https://archipelago.indeehub.studio', prod: 'https://archipelago.indeehub.studio' },
|
||||
'bitcoin': { dev: 'http://localhost:8332', prod: 'http://localhost:8332' },
|
||||
'btcpay-server': { dev: 'http://localhost:23000', prod: 'http://localhost:23000' },
|
||||
'homeassistant': { dev: 'http://localhost:8123', prod: 'http://localhost:8123' },
|
||||
'grafana': { dev: 'http://localhost:3000', prod: 'http://localhost:3000' },
|
||||
'endurain': { dev: 'http://localhost:8080', prod: 'http://localhost:8080' },
|
||||
'fedimint': { dev: 'http://localhost:8175', prod: 'http://192.168.1.228:8175' },
|
||||
'fedimint-gateway': { dev: 'http://localhost:8176', prod: 'http://192.168.1.228:8176' },
|
||||
'morphos-server': { dev: 'http://localhost:8081', prod: 'http://localhost:8081' },
|
||||
'lightning-stack': { dev: 'http://localhost:9735', prod: 'http://localhost:9735' },
|
||||
'mempool': { dev: 'http://localhost:4080', prod: 'http://localhost:4080' },
|
||||
'ollama': { dev: 'http://localhost:11434', prod: 'http://localhost:11434' },
|
||||
'searxng': { dev: 'http://localhost:8888', prod: 'http://localhost:8888' },
|
||||
'onlyoffice': { dev: 'http://localhost:9980', prod: 'http://localhost:9980' },
|
||||
'penpot': { dev: 'http://localhost:9001', prod: 'http://localhost:9001' },
|
||||
'nextcloud': { dev: 'http://localhost:8085', prod: 'http://localhost:8085' },
|
||||
'vaultwarden': { dev: 'http://localhost:8082', prod: 'http://localhost:8082' },
|
||||
'jellyfin': { dev: 'http://localhost:8096', prod: 'http://localhost:8096' },
|
||||
'photoprism': { dev: 'http://localhost:2342', prod: 'http://localhost:2342' },
|
||||
'immich': { dev: 'http://localhost:2283', prod: 'http://localhost:2283' },
|
||||
'filebrowser': { dev: 'http://localhost:8083', prod: 'http://localhost:8083' },
|
||||
'nginx-proxy-manager': { dev: 'http://localhost:81', prod: 'http://localhost:81' },
|
||||
'portainer': { dev: 'http://localhost:9000', prod: 'http://localhost:9000' },
|
||||
'uptime-kuma': { dev: 'http://localhost:3001', prod: 'http://localhost:3001' },
|
||||
'tailscale': { dev: 'http://localhost:8240', prod: 'http://localhost:8240' },
|
||||
'lnd': { dev: 'http://localhost:8081', prod: 'http://localhost:8081' },
|
||||
'bitcoin-knots': { dev: 'http://localhost:8334', prod: 'http://localhost:8334' },
|
||||
'botfights': { dev: 'https://botfights.net', prod: 'https://botfights.net' },
|
||||
'nwnn': { dev: 'https://nwnn.l484.com', prod: 'https://nwnn.l484.com' },
|
||||
'484-kitchen': { dev: 'https://484.kitchen', prod: 'https://484.kitchen' },
|
||||
'call-the-operator': { dev: 'https://cta.tx1138.com', prod: 'https://cta.tx1138.com' },
|
||||
'arch-presentation': { dev: 'https://present.l484.com', prod: 'https://present.l484.com' },
|
||||
'syntropy-institute': { dev: 'https://syntropy.institute', prod: 'https://syntropy.institute' },
|
||||
't-zero': { dev: 'https://teeminuszero.net', prod: 'https://teeminuszero.net' },
|
||||
}
|
||||
|
||||
/** V3 onion addresses are 56+ chars + .onion. Placeholders like "btcpay.onion" are not real. */
|
||||
export function isRealOnionAddress(addr: string | undefined): boolean {
|
||||
return !!(addr && addr.endsWith('.onion') && addr.length >= 60 && addr.length <= 70)
|
||||
}
|
||||
|
||||
export function getStatusClass(state: PackageState, health?: string | null): string {
|
||||
if (state === PackageState.Running && health === 'starting') return 'bg-yellow-500/20 text-yellow-200 border border-yellow-500/30'
|
||||
if (state === PackageState.Running && health === 'unhealthy') return 'bg-orange-500/20 text-orange-200 border border-orange-500/30'
|
||||
switch (state) {
|
||||
case PackageState.Running:
|
||||
return 'bg-green-500/20 text-green-200 border border-green-500/30'
|
||||
case PackageState.Stopped:
|
||||
return 'bg-gray-500/20 text-gray-200 border border-gray-500/30'
|
||||
case PackageState.Exited:
|
||||
return 'bg-red-500/20 text-red-200 border border-red-500/30'
|
||||
case PackageState.Starting:
|
||||
case PackageState.Stopping:
|
||||
case PackageState.Restarting:
|
||||
return 'bg-yellow-500/20 text-yellow-200 border border-yellow-500/30'
|
||||
case PackageState.Installing:
|
||||
return 'bg-blue-500/20 text-blue-200 border border-blue-500/30'
|
||||
default:
|
||||
return 'bg-gray-500/20 text-gray-200 border border-gray-500/30'
|
||||
}
|
||||
}
|
||||
|
||||
export function getStatusDotClass(state: PackageState, health?: string | null): string {
|
||||
if (state === PackageState.Running && health === 'starting') return 'bg-yellow-400 animate-pulse'
|
||||
if (state === PackageState.Running && health === 'unhealthy') return 'bg-orange-400 animate-pulse'
|
||||
switch (state) {
|
||||
case PackageState.Running:
|
||||
return 'bg-green-400'
|
||||
case PackageState.Stopped:
|
||||
return 'bg-gray-400'
|
||||
case PackageState.Exited:
|
||||
return 'bg-red-400 animate-pulse'
|
||||
case PackageState.Starting:
|
||||
case PackageState.Stopping:
|
||||
case PackageState.Restarting:
|
||||
return 'bg-yellow-400 animate-pulse'
|
||||
case PackageState.Installing:
|
||||
return 'bg-blue-400 animate-pulse'
|
||||
default:
|
||||
return 'bg-gray-400'
|
||||
}
|
||||
}
|
||||
|
||||
export function getStatusLabel(state: PackageState, health?: string | null): string {
|
||||
if (state === PackageState.Running && health === 'starting') return 'starting up'
|
||||
if (state === PackageState.Running && health === 'unhealthy') return 'unhealthy'
|
||||
if (state === PackageState.Running && health === 'healthy') return 'healthy'
|
||||
if (state === PackageState.Exited) return 'crashed'
|
||||
return state
|
||||
}
|
||||
Reference in New Issue
Block a user