- Replaced cyan color with yellow in various UI components for a cohesive visual theme. - Improved focus styles for controller navigation, adding subtle grow and glow effects to sidebar items and containers. - Enhanced controller navigation logic to support direct focus on app containers in the Marketplace and My Apps sections. - Introduced wheel scrolling support for better navigation experience within scrollable areas. - Removed unused audio tone function from useLoginSounds.ts to streamline code.
485 lines
19 KiB
Vue
485 lines
19 KiB
Vue
<template>
|
|
<div>
|
|
<div class="mb-8 flex items-start justify-between">
|
|
<div>
|
|
<div class="min-h-[4.5rem]">
|
|
<h1 class="text-3xl font-bold text-white mb-2 drop-shadow-[0_2px_8px_rgba(0,0,0,0.6)]">
|
|
{{ line1Text }}<span v-if="showCaretLine1" class="typing-caret"></span>
|
|
</h1>
|
|
<p class="text-white/80">
|
|
{{ line2Text }}<span v-if="showCaretLine2" class="typing-caret"></span>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<!-- Compact Status Indicator -->
|
|
<div class="flex items-center gap-2 px-4 py-2 glass-card rounded-lg">
|
|
<div class="relative">
|
|
<div class="w-2 h-2 rounded-full bg-green-400"></div>
|
|
<div class="absolute inset-0 w-2 h-2 rounded-full bg-green-400 animate-ping opacity-75"></div>
|
|
</div>
|
|
<span class="text-sm font-medium text-white">Online</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Section Overviews - 2advanced staggered animation (hidden until typing starts, then animate with typing) -->
|
|
<div
|
|
class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8 transition-opacity duration-300"
|
|
:class="{ 'opacity-0 pointer-events-none': showWelcomeBlock && !animateCards }"
|
|
>
|
|
<!-- My Apps Overview -->
|
|
<div
|
|
data-controller-container
|
|
tabindex="0"
|
|
class="home-card controller-focusable"
|
|
:class="{ 'home-card-animate': animateCards }"
|
|
style="--card-stagger: 0"
|
|
>
|
|
<div class="home-card-shell">
|
|
<div class="home-card-inner p-6 flex flex-col h-full min-h-0">
|
|
<div class="home-card-header flex items-start justify-between mb-4 shrink-0">
|
|
<div class="home-card-text">
|
|
<h2 class="text-xl font-semibold text-white mb-1">My Apps</h2>
|
|
<p class="text-sm text-white/70">Manage your installed applications</p>
|
|
</div>
|
|
<RouterLink to="/dashboard/apps" class="text-white/60 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="M9 5l7 7-7 7" />
|
|
</svg>
|
|
</RouterLink>
|
|
</div>
|
|
<div class="home-card-stats grid grid-cols-2 gap-4 mb-4 flex-1 min-h-0">
|
|
<div class="p-4 bg-white/5 rounded-lg">
|
|
<p class="text-xs text-white/60 mb-1">Installed</p>
|
|
<p class="text-2xl font-bold text-white">{{ appCount }}</p>
|
|
</div>
|
|
<div class="p-4 bg-white/5 rounded-lg">
|
|
<p class="text-xs text-white/60 mb-1">Running</p>
|
|
<p class="text-2xl font-bold text-white">{{ runningCount }}</p>
|
|
</div>
|
|
</div>
|
|
<div class="home-card-buttons flex gap-2 mt-auto pt-4 shrink-0">
|
|
<RouterLink to="/dashboard/marketplace" class="home-card-btn flex-1 px-4 py-2 glass-button rounded-lg text-sm font-medium text-center transition-colors">
|
|
Browse Store
|
|
</RouterLink>
|
|
<RouterLink to="/dashboard/apps" class="home-card-btn flex-1 px-4 py-2 glass-button rounded-lg text-sm font-medium text-center transition-colors">
|
|
Manage Apps
|
|
</RouterLink>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Cloud Overview -->
|
|
<div
|
|
data-controller-container
|
|
tabindex="0"
|
|
class="home-card controller-focusable"
|
|
:class="{ 'home-card-animate': animateCards }"
|
|
style="--card-stagger: 1"
|
|
>
|
|
<div class="home-card-shell">
|
|
<div class="home-card-inner p-6 flex flex-col h-full min-h-0">
|
|
<div class="home-card-header flex items-start justify-between mb-4 shrink-0">
|
|
<div class="home-card-text">
|
|
<h2 class="text-xl font-semibold text-white mb-1">Cloud</h2>
|
|
<p class="text-sm text-white/70">Cloud services and storage</p>
|
|
</div>
|
|
<RouterLink to="/dashboard/cloud" class="text-white/60 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="M9 5l7 7-7 7" />
|
|
</svg>
|
|
</RouterLink>
|
|
</div>
|
|
<div class="home-card-stats grid grid-cols-2 gap-4 mb-4 flex-1 min-h-0">
|
|
<div class="p-4 bg-white/5 rounded-lg">
|
|
<p class="text-xs text-white/60 mb-1">Storage Used</p>
|
|
<p class="text-2xl font-bold text-white">2.4 GB</p>
|
|
</div>
|
|
<div class="p-4 bg-white/5 rounded-lg">
|
|
<p class="text-xs text-white/60 mb-1">Folders</p>
|
|
<p class="text-2xl font-bold text-white">5</p>
|
|
</div>
|
|
</div>
|
|
<div class="home-card-buttons flex gap-2 mt-auto pt-4 shrink-0">
|
|
<RouterLink to="/dashboard/cloud" class="home-card-btn flex-1 px-4 py-2 glass-button rounded-lg text-sm font-medium text-center transition-colors">
|
|
View Folders
|
|
</RouterLink>
|
|
<button class="home-card-btn flex-1 px-4 py-2 glass-button rounded-lg text-sm font-medium transition-colors" @click="() => {}">
|
|
Upload Files
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Network Overview -->
|
|
<div
|
|
data-controller-container
|
|
tabindex="0"
|
|
class="home-card controller-focusable"
|
|
:class="{ 'home-card-animate': animateCards }"
|
|
style="--card-stagger: 2"
|
|
>
|
|
<div class="home-card-shell">
|
|
<div class="home-card-inner p-6 flex flex-col h-full min-h-0">
|
|
<div class="home-card-header flex items-start justify-between mb-4 shrink-0">
|
|
<div class="home-card-text">
|
|
<h2 class="text-xl font-semibold text-white mb-1">Network</h2>
|
|
<p class="text-sm text-white/70">Network infrastructure and Web3 services</p>
|
|
</div>
|
|
<RouterLink to="/dashboard/server" class="text-white/60 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="M9 5l7 7-7 7" />
|
|
</svg>
|
|
</RouterLink>
|
|
</div>
|
|
<div class="home-card-stats space-y-3 mb-4 flex-1 min-h-0">
|
|
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
|
|
<div class="flex items-center gap-3">
|
|
<div class="w-2 h-2 rounded-full bg-green-400"></div>
|
|
<span class="text-sm text-white/80">Services Status</span>
|
|
</div>
|
|
<span class="text-sm text-green-400 font-medium">All Running</span>
|
|
</div>
|
|
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
|
|
<div class="flex items-center gap-3">
|
|
<div class="w-2 h-2 rounded-full bg-green-400"></div>
|
|
<span class="text-sm text-white/80">Connectivity</span>
|
|
</div>
|
|
<span class="text-sm text-green-400 font-medium">Connected</span>
|
|
</div>
|
|
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
|
|
<div class="flex items-center gap-3">
|
|
<div class="w-2 h-2 rounded-full bg-blue-400"></div>
|
|
<span class="text-sm text-white/80">Connected Nodes</span>
|
|
</div>
|
|
<span class="text-sm text-white/80 font-medium">12</span>
|
|
</div>
|
|
</div>
|
|
<div class="home-card-buttons flex gap-2 mt-auto pt-4 shrink-0">
|
|
<RouterLink to="/dashboard/server" class="home-card-btn flex-1 px-4 py-2 glass-button rounded-lg text-sm font-medium text-center transition-colors">
|
|
Manage Network
|
|
</RouterLink>
|
|
<button class="home-card-btn px-4 py-2 glass-button rounded-lg text-sm font-medium transition-colors" @click="() => {}">
|
|
<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="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>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Web5 Overview -->
|
|
<div
|
|
data-controller-container
|
|
tabindex="0"
|
|
class="home-card controller-focusable"
|
|
:class="{ 'home-card-animate': animateCards }"
|
|
style="--card-stagger: 3"
|
|
>
|
|
<div class="home-card-shell">
|
|
<div class="home-card-inner p-6 flex flex-col h-full min-h-0">
|
|
<div class="home-card-header flex items-start justify-between mb-4 shrink-0">
|
|
<div class="home-card-text">
|
|
<h2 class="text-xl font-semibold text-white mb-1">Web5</h2>
|
|
<p class="text-sm text-white/70">Decentralized identity and data protocols</p>
|
|
</div>
|
|
<RouterLink to="/dashboard/web5" class="text-white/60 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="M9 5l7 7-7 7" />
|
|
</svg>
|
|
</RouterLink>
|
|
</div>
|
|
<div class="home-card-stats space-y-3 mb-4 flex-1 min-h-0">
|
|
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
|
|
<div class="flex items-center gap-3">
|
|
<div class="w-2 h-2 rounded-full bg-green-400"></div>
|
|
<span class="text-sm text-white/80">DID Status</span>
|
|
</div>
|
|
<span class="text-sm text-green-400 font-medium">Active</span>
|
|
</div>
|
|
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
|
|
<div class="flex items-center gap-3">
|
|
<div class="w-2 h-2 rounded-full bg-green-400"></div>
|
|
<span class="text-sm text-white/80">DWN Sync</span>
|
|
</div>
|
|
<span class="text-sm text-green-400 font-medium">Synced</span>
|
|
</div>
|
|
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
|
|
<div class="flex items-center gap-3">
|
|
<span class="text-lg text-orange-500 font-bold">₿</span>
|
|
<span class="text-sm text-white/80">Networking Profits</span>
|
|
</div>
|
|
<span class="text-sm text-orange-500 font-medium">₿0.024</span>
|
|
</div>
|
|
</div>
|
|
<div class="home-card-buttons flex gap-2 mt-auto pt-4 shrink-0">
|
|
<RouterLink to="/dashboard/web5" class="home-card-btn flex-1 px-4 py-2 glass-button rounded-lg text-sm font-medium text-center transition-colors">
|
|
Manage Web5
|
|
</RouterLink>
|
|
<button class="home-card-btn px-4 py-2 glass-button rounded-lg text-sm font-medium transition-colors" @click="() => {}">
|
|
<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="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>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Quick Actions - Hidden for now -->
|
|
<!--
|
|
<div class="path-option-card cursor-default px-6 py-6">
|
|
<h2 class="text-xl font-semibold text-white/96 mb-4">Quick Actions</h2>
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<RouterLink
|
|
to="/dashboard/marketplace"
|
|
class="path-action-button path-action-button--continue flex items-center justify-center gap-3"
|
|
>
|
|
<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="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z" />
|
|
</svg>
|
|
<span>Browse App Store</span>
|
|
</RouterLink>
|
|
|
|
<RouterLink
|
|
to="/dashboard/apps"
|
|
class="path-action-button path-action-button--continue flex items-center justify-center gap-3"
|
|
>
|
|
<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="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" />
|
|
</svg>
|
|
<span>Manage My Apps</span>
|
|
</RouterLink>
|
|
|
|
<RouterLink
|
|
to="/dashboard/server"
|
|
class="path-action-button path-action-button--continue flex items-center justify-center gap-3"
|
|
>
|
|
<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="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01" />
|
|
</svg>
|
|
<span>Server Settings</span>
|
|
</RouterLink>
|
|
</div>
|
|
</div>
|
|
-->
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, ref, watch, onBeforeUnmount } from 'vue'
|
|
import { RouterLink } from 'vue-router'
|
|
import { useAppStore } from '../stores/app'
|
|
import { useLoginTransitionStore } from '../stores/loginTransition'
|
|
import { PackageState } from '../types/api'
|
|
import { playTypingSound } from '@/composables/useLoginSounds'
|
|
|
|
const store = useAppStore()
|
|
const loginTransition = useLoginTransitionStore()
|
|
|
|
const LINE1 = "Welcome Noderunner"
|
|
const LINE2 = "Here's an overview of your sovereign life"
|
|
const MS_PER_CHAR = 55
|
|
|
|
const displayLine1 = ref('')
|
|
const displayLine2 = ref('')
|
|
const showCaretLine1 = ref(false)
|
|
const showCaretLine2 = ref(false)
|
|
const showWelcomeBlock = ref(false)
|
|
const hasTypedWelcome = ref(false)
|
|
const animateCards = ref(false)
|
|
let typingInterval: ReturnType<typeof setInterval> | null = null
|
|
|
|
const line1Text = computed(() =>
|
|
showWelcomeBlock.value ? displayLine1.value : LINE1
|
|
)
|
|
const line2Text = computed(() =>
|
|
showWelcomeBlock.value ? displayLine2.value : LINE2
|
|
)
|
|
|
|
onBeforeUnmount(() => {
|
|
if (typingInterval) clearInterval(typingInterval)
|
|
})
|
|
|
|
watch(() => loginTransition.pendingWelcomeTyping, (pending) => {
|
|
if (pending) showWelcomeBlock.value = true
|
|
})
|
|
|
|
watch(() => loginTransition.startWelcomeTyping, (shouldStart) => {
|
|
if (!shouldStart || hasTypedWelcome.value) return
|
|
hasTypedWelcome.value = true
|
|
showWelcomeBlock.value = true
|
|
displayLine1.value = ''
|
|
displayLine2.value = ''
|
|
showCaretLine1.value = true
|
|
showCaretLine2.value = false
|
|
|
|
playTypingSound()
|
|
animateCards.value = true
|
|
|
|
let i = 0
|
|
typingInterval = setInterval(() => {
|
|
if (i < LINE1.length) {
|
|
displayLine1.value = LINE1.slice(0, i + 1)
|
|
i++
|
|
} else if (i < LINE1.length + LINE2.length) {
|
|
showCaretLine1.value = false
|
|
showCaretLine2.value = true
|
|
displayLine2.value = LINE2.slice(0, i - LINE1.length + 1)
|
|
i++
|
|
} else {
|
|
if (typingInterval) clearInterval(typingInterval)
|
|
typingInterval = null
|
|
showCaretLine2.value = false
|
|
loginTransition.setStartWelcomeTyping(false)
|
|
}
|
|
}, MS_PER_CHAR)
|
|
}, { immediate: true })
|
|
|
|
// @ts-ignore - Computed kept for future use
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
const version = computed(() => store.serverInfo?.version || '0.0.0')
|
|
const packages = computed(() => store.packages)
|
|
const appCount = computed(() => Object.keys(packages.value).length)
|
|
const runningCount = computed(() =>
|
|
Object.values(packages.value).filter(pkg => pkg.state === PackageState.Running).length
|
|
)
|
|
</script>
|
|
|
|
<style scoped>
|
|
.typing-caret::after {
|
|
content: '';
|
|
display: inline-block;
|
|
width: 3px;
|
|
height: 1.1em;
|
|
background: #fbbf24;
|
|
margin-left: 2px;
|
|
vertical-align: text-bottom;
|
|
animation: caret-blink 0.7s step-end infinite;
|
|
}
|
|
|
|
@keyframes caret-blink {
|
|
0%, 100% { opacity: 1; }
|
|
50% { opacity: 0; }
|
|
}
|
|
|
|
/* 2advanced-style card animation sequence */
|
|
.home-card {
|
|
min-height: 280px;
|
|
}
|
|
|
|
.home-card-shell {
|
|
background-color: rgba(0, 0, 0, 0.65);
|
|
backdrop-filter: blur(18px);
|
|
-webkit-backdrop-filter: blur(18px);
|
|
border-radius: 1rem;
|
|
overflow: hidden;
|
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
|
|
border: 1px solid transparent;
|
|
height: 100%;
|
|
}
|
|
|
|
.home-card-animate .home-card-shell {
|
|
animation: card-fly-in 1.2s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
|
|
animation-delay: calc(var(--card-stagger) * 0.18s);
|
|
opacity: 0;
|
|
transform: translateY(50px) scale(0.92);
|
|
}
|
|
|
|
@keyframes card-fly-in {
|
|
0% {
|
|
opacity: 0;
|
|
transform: translateY(50px) scale(0.92);
|
|
border-color: transparent;
|
|
}
|
|
75% {
|
|
opacity: 1;
|
|
transform: translateY(0) scale(1);
|
|
border-color: transparent;
|
|
}
|
|
100% {
|
|
opacity: 1;
|
|
transform: translateY(0) scale(1);
|
|
border-color: rgba(255, 255, 255, 0.18);
|
|
}
|
|
}
|
|
|
|
.home-card-inner {
|
|
overflow: hidden;
|
|
opacity: 0;
|
|
}
|
|
|
|
.home-card-animate .home-card-inner {
|
|
animation: inner-draw 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
|
|
animation-delay: calc(var(--card-stagger) * 0.18s + 0.9s);
|
|
}
|
|
|
|
@keyframes inner-draw {
|
|
0% {
|
|
opacity: 0;
|
|
clip-path: inset(0 100% 0 0);
|
|
}
|
|
15% {
|
|
opacity: 1;
|
|
}
|
|
100% {
|
|
opacity: 1;
|
|
clip-path: inset(0 0 0 0);
|
|
}
|
|
}
|
|
|
|
.home-card-text {
|
|
overflow: hidden;
|
|
}
|
|
|
|
.home-card-stats {
|
|
overflow: hidden;
|
|
}
|
|
|
|
.home-card-btn {
|
|
opacity: 0;
|
|
transform: scale(0.5);
|
|
border-color: transparent;
|
|
}
|
|
|
|
.home-card-animate .home-card-btn {
|
|
animation: btn-pop 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
|
|
animation-delay: calc(var(--card-stagger) * 0.18s + 1.5s);
|
|
}
|
|
|
|
@keyframes btn-pop {
|
|
0% {
|
|
opacity: 0;
|
|
transform: scale(0.5);
|
|
border-color: transparent;
|
|
}
|
|
85% {
|
|
opacity: 1;
|
|
transform: scale(1);
|
|
border-color: transparent;
|
|
}
|
|
100% {
|
|
opacity: 1;
|
|
transform: scale(1);
|
|
border-color: rgba(255, 255, 255, 0.18);
|
|
}
|
|
}
|
|
|
|
/* When not animating, show everything */
|
|
.home-card:not(.home-card-animate) .home-card-inner,
|
|
.home-card:not(.home-card-animate) .home-card-btn {
|
|
opacity: 1;
|
|
animation: none;
|
|
clip-path: none;
|
|
transform: none;
|
|
border-color: rgba(255, 255, 255, 0.18);
|
|
}
|
|
|
|
.home-card:not(.home-card-animate) .home-card-shell {
|
|
border-color: rgba(255, 255, 255, 0.18);
|
|
}
|
|
</style>
|