Files
MeshChatX/meshchatx/src/frontend/components/TutorialModal.vue

1749 lines
102 KiB
Vue

<template>
<v-dialog
v-if="!isPage"
v-model="visible"
:fullscreen="isMobile"
max-width="800"
transition="dialog-bottom-transition"
class="tutorial-dialog"
persistent
@update:model-value="onVisibleUpdate"
>
<v-card class="flex flex-col h-full bg-white dark:bg-zinc-950 border-0 overflow-hidden relative">
<!-- Settings Controls -->
<div class="absolute top-4 left-4 z-50 flex items-center gap-1">
<LanguageSelector @language-change="onLanguageChange" />
<button
type="button"
class="rounded-full p-1.5 sm:p-2 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-zinc-800 transition-colors"
:title="config?.theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'"
@click="toggleTheme"
>
<MaterialDesignIcon
:icon-name="config?.theme === 'dark' ? 'brightness-6' : 'brightness-4'"
class="w-5 h-5 sm:w-6 sm:h-6"
/>
</button>
</div>
<!-- Progress Bar -->
<div class="w-full h-1.5 bg-gray-100 dark:bg-zinc-900 overflow-hidden flex">
<div
v-for="step in totalSteps"
:key="step"
class="h-full transition-all duration-500 ease-out"
:class="[
currentStep >= step ? 'bg-blue-500' : 'bg-transparent',
currentStep === step ? 'flex-[2]' : 'flex-1',
]"
:style="{ borderRight: step < totalSteps ? '1px solid rgba(0,0,0,0.05)' : 'none' }"
></div>
</div>
<!-- Content Area -->
<v-card-text class="flex-1 overflow-y-auto px-6 md:px-12 py-10 relative">
<transition name="fade-slide" mode="out-in">
<!-- Step 1: Welcome -->
<div v-if="currentStep === 1" key="step1" class="flex flex-col items-center text-center space-y-6">
<div class="relative">
<div class="w-24 h-24 bg-blue-500/10 rounded-3xl rotate-12 absolute -inset-2"></div>
<img :src="logoUrl" class="w-24 h-24 relative z-10 p-2" />
</div>
<div class="space-y-2">
<h1 class="text-4xl font-black tracking-tight text-gray-900 dark:text-white">
{{ $t("tutorial.welcome") }} <span class="text-blue-500">MeshChatX</span>
</h1>
<p class="text-lg text-gray-600 dark:text-zinc-400 max-w-md mx-auto">
{{ $t("tutorial.welcome_desc") }}
</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 w-full mt-8">
<div
class="flex items-start gap-4 p-4 rounded-2xl bg-gray-50 dark:bg-zinc-900 text-left border border-gray-100 dark:border-zinc-800 transition-all hover:scale-[1.03] hover:shadow-xl hover:z-10"
>
<v-icon icon="mdi-shield-lock" color="blue" size="32"></v-icon>
<div>
<div class="font-bold text-gray-900 dark:text-white">
{{ $t("tutorial.security") }}
</div>
<div class="text-sm text-gray-900 dark:text-white">
{{ $t("tutorial.security_desc") }}
</div>
</div>
</div>
<div
class="flex items-start gap-4 p-4 rounded-2xl bg-gray-50 dark:bg-zinc-900 text-left border border-gray-100 dark:border-zinc-800 transition-all hover:scale-[1.03] hover:shadow-xl hover:z-10"
>
<v-icon icon="mdi-map-marker-path" color="purple" size="32"></v-icon>
<div>
<div class="font-bold text-gray-900 dark:text-white">{{ $t("tutorial.maps") }}</div>
<div class="text-sm text-gray-900 dark:text-white">
{{ $t("tutorial.maps_desc") }}
</div>
</div>
</div>
<div
class="flex items-start gap-4 p-4 rounded-2xl bg-gray-50 dark:bg-zinc-900 text-left border border-gray-100 dark:border-zinc-800 transition-all hover:scale-[1.03] hover:shadow-xl hover:z-10"
>
<v-icon icon="mdi-phone" color="green" size="32"></v-icon>
<div>
<div class="font-bold text-gray-900 dark:text-white">
{{ $t("tutorial.voice") }}
</div>
<div class="text-sm text-gray-900 dark:text-white">
{{ $t("tutorial.voice_desc") }}
</div>
</div>
</div>
<div
class="flex items-start gap-4 p-4 rounded-2xl bg-gray-50 dark:bg-zinc-900 text-left border border-gray-100 dark:border-zinc-800 transition-all hover:scale-[1.03] hover:shadow-xl hover:z-10"
>
<v-icon icon="mdi-tools" color="orange" size="32"></v-icon>
<div>
<div class="font-bold text-gray-900 dark:text-white">
{{ $t("tutorial.tools") }}
</div>
<div class="text-sm text-gray-900 dark:text-white">
{{ $t("tutorial.tools_desc") }}
</div>
</div>
</div>
<div
class="flex items-start gap-4 p-4 rounded-2xl bg-gray-50 dark:bg-zinc-900 text-left border border-gray-100 dark:border-zinc-800 transition-all hover:scale-[1.03] hover:shadow-xl hover:z-10"
>
<v-icon icon="mdi-database-search" color="teal" size="32"></v-icon>
<div>
<div class="font-bold text-gray-900 dark:text-white">
{{ $t("tutorial.archiver") }}
</div>
<div class="text-sm text-gray-900 dark:text-white">
{{ $t("tutorial.archiver_desc") }}
</div>
</div>
</div>
<div
class="flex items-start gap-4 p-4 rounded-2xl bg-gray-50 dark:bg-zinc-900 text-left border border-gray-100 dark:border-zinc-800 transition-all hover:scale-[1.03] hover:shadow-xl hover:z-10"
>
<v-icon icon="mdi-account-cancel" color="amber" size="32"></v-icon>
<div>
<div class="font-bold text-gray-900 dark:text-white">
{{ $t("tutorial.banishment") }}
</div>
<div class="text-sm text-gray-900 dark:text-white">
{{ $t("tutorial.banishment_desc") }}
</div>
</div>
</div>
<div
class="flex items-start gap-4 p-4 rounded-2xl bg-gray-50 dark:bg-zinc-900 text-left border border-gray-100 dark:border-zinc-800 transition-all hover:scale-[1.03] hover:shadow-xl hover:z-10"
>
<v-icon icon="mdi-keyboard-outline" color="red" size="32"></v-icon>
<div>
<div class="font-bold text-gray-900 dark:text-white">
{{ $t("tutorial.palette") }}
</div>
<div class="text-sm text-gray-900 dark:text-white">
{{ $t("tutorial.palette_desc") }}
</div>
</div>
</div>
<div
class="flex items-start gap-4 p-4 rounded-2xl bg-gray-50 dark:bg-zinc-900 text-left border border-gray-100 dark:border-zinc-800 transition-all hover:scale-[1.03] hover:shadow-xl hover:z-10"
>
<v-icon icon="mdi-translate" color="cyan" size="32"></v-icon>
<div>
<div class="font-bold text-gray-900 dark:text-white">{{ $t("tutorial.i18n") }}</div>
<div class="text-sm text-gray-900 dark:text-white">
{{ $t("tutorial.i18n_desc") }}
</div>
</div>
</div>
</div>
<div
class="w-full flex justify-end items-center gap-2 mt-4 px-4 text-gray-400 dark:text-zinc-500"
>
<v-icon icon="mdi-plus" size="16"></v-icon>
<span class="text-xs font-bold uppercase tracking-widest">{{
$t("tutorial.more_features")
}}</span>
</div>
</div>
<!-- Step 2: Add Interface -->
<div v-else-if="currentStep === 2" key="step2" class="space-y-6">
<div class="text-center space-y-2">
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">
{{ $t("tutorial.connect") }}
</h2>
<p class="text-gray-600 dark:text-zinc-400 text-base">
{{ $t("tutorial.connect_desc") }}
</p>
</div>
<div v-if="discoveryOption === null" class="flex flex-col items-center gap-6 py-4">
<div
class="bg-blue-500/10 dark:bg-blue-500/20 p-6 rounded-[2rem] text-center space-y-4 border border-blue-500/20 max-w-md"
>
<v-icon icon="mdi-account-search" color="blue" size="48"></v-icon>
<div class="text-lg font-bold text-gray-900 dark:text-white">
{{
$t("tutorial.discovery_question") ||
"Do you want to use community interface discovering and auto-connect?"
}}
</div>
<p class="text-sm text-gray-600 dark:text-zinc-400">
{{
$t("tutorial.discovery_desc") ||
"This allows MeshChatX to automatically find and connect to public community nodes near you or on the internet."
}}
</p>
<div class="flex gap-3 justify-center pt-2">
<button
type="button"
class="px-6 py-2 rounded-xl bg-blue-600 hover:bg-blue-500 text-white font-bold shadow-lg transition-all"
:disabled="savingDiscovery"
@click="useDiscovery"
>
<v-progress-circular
v-if="savingDiscovery"
indeterminate
size="16"
width="2"
class="mr-2"
></v-progress-circular>
{{ $t("tutorial.yes") || "Yes, use discovery" }}
</button>
<button
type="button"
class="px-6 py-2 rounded-xl border border-gray-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 text-gray-700 dark:text-zinc-300 font-bold shadow-sm transition-all"
@click="discoveryOption = 'no'"
>
{{ $t("tutorial.no") || "No, manual setup" }}
</button>
</div>
</div>
</div>
<div v-else class="space-y-4">
<!-- Discovered Interfaces (if any) -->
<div
v-if="sortedDiscoveredInterfaces.length > 0"
class="bg-emerald-500/5 dark:bg-emerald-500/10 rounded-3xl p-4 border border-emerald-500/20"
>
<div class="flex items-center gap-2 mb-3 px-1 text-sm">
<v-icon icon="mdi-radar" color="emerald"></v-icon>
<span class="font-bold text-gray-900 dark:text-white">Discovered Interfaces</span>
</div>
<div class="space-y-3 max-h-[300px] overflow-y-auto pr-2 custom-scrollbar">
<div
v-for="iface in sortedDiscoveredInterfaces"
:key="iface.discovery_hash || iface.name"
class="interface-card group !p-3 transition-all duration-300"
>
<div class="flex gap-3 items-start relative">
<div class="interface-card__icon !w-10 !h-10 !rounded-xl shrink-0">
<MaterialDesignIcon
:icon-name="getDiscoveryIcon(iface)"
class="w-5 h-5"
/>
</div>
<div class="flex-1 min-w-0 space-y-1">
<div class="flex items-center gap-2 flex-nowrap min-w-0">
<div
class="text-sm font-bold text-gray-900 dark:text-white truncate min-w-0"
>
{{ iface.name }}
</div>
<span class="type-chip !text-[9px] !px-1.5 shrink-0">{{
iface.type
}}</span>
</div>
<div class="flex items-center gap-2 flex-wrap">
<span
v-if="iface.value"
class="text-[9px] font-bold text-blue-600 dark:text-blue-400 shrink-0"
>
Stamps: {{ iface.value }}
</span>
</div>
<div class="flex flex-wrap gap-1.5 text-[10px] text-gray-500">
<span>Hops: {{ iface.hops }}</span>
<span class="capitalize shrink-0">{{ iface.status }}</span>
<span v-if="iface.last_heard" class="shrink-0">
{{ formatLastHeard(iface.last_heard) }}
</span>
</div>
<div
class="flex flex-col gap-0.5 pt-1 text-[9px] text-gray-400 dark:text-zinc-500 min-w-0"
>
<div
v-if="iface.reachable_on"
class="flex items-center gap-1.5 hover:text-blue-500 cursor-pointer min-w-0"
@click="
copyToClipboard(
`${iface.reachable_on}:${iface.port}`,
'Address'
)
"
>
<MaterialDesignIcon
icon-name="link-variant"
class="w-3 h-3 shrink-0"
/>
<span class="truncate"
>Address: {{ iface.reachable_on }}:{{ iface.port }}</span
>
</div>
<div
v-if="iface.transport_id"
class="flex items-center gap-1.5 hover:text-blue-500 cursor-pointer min-w-0"
@click="copyToClipboard(iface.transport_id, 'Transport ID')"
>
<MaterialDesignIcon
icon-name="identifier"
class="w-3 h-3 shrink-0"
/>
<span class="truncate font-mono"
>Transport ID: {{ iface.transport_id }}</span
>
</div>
<div
v-if="iface.network_id"
class="flex items-center gap-1.5 hover:text-blue-500 cursor-pointer min-w-0"
@click="copyToClipboard(iface.network_id, 'Network ID')"
>
<MaterialDesignIcon icon-name="lan" class="w-3 h-3 shrink-0" />
<span class="truncate font-mono"
>Network ID: {{ iface.network_id }}</span
>
</div>
</div>
</div>
<div class="flex items-center gap-1 shrink-0">
<span
class="text-[8px] bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 px-2 py-0.5 rounded-full font-bold uppercase tracking-wider"
>Heard</span
>
</div>
</div>
</div>
</div>
</div>
<!-- Community Interfaces -->
<div
class="bg-gray-50 dark:bg-zinc-900 rounded-3xl p-3 border border-gray-100 dark:border-zinc-800"
>
<div class="flex items-center gap-2 mb-3 px-2 text-sm">
<v-icon icon="mdi-web" color="blue"></v-icon>
<span class="font-bold text-gray-900 dark:text-white">{{
$t("tutorial.suggested_networks")
}}</span>
</div>
<div class="space-y-2 max-h-[260px] overflow-y-auto pr-2">
<div
v-for="iface in communityInterfaces"
:key="iface.name"
class="flex items-center justify-between p-3 bg-white dark:bg-zinc-800 rounded-xl border border-gray-100 dark:border-zinc-700 hover:border-blue-400 transition-colors cursor-pointer"
@click="selectCommunityInterface(iface)"
>
<div class="flex flex-col">
<span class="font-bold text-gray-900 dark:text-white text-sm">{{
iface.name
}}</span>
<span class="text-[11px] text-gray-500 dark:text-zinc-400"
>{{ iface.target_host }}:{{ iface.target_port }}</span
>
</div>
<div class="flex items-center gap-2">
<span
v-if="iface.online"
class="flex items-center gap-1 text-[9px] font-bold text-green-500 uppercase tracking-[0.2em]"
>
<span
class="w-1.5 h-1.5 rounded-full bg-green-500 animate-pulse"
></span>
{{ $t("tutorial.online") }}
</span>
<button
type="button"
class="px-3 py-1 text-[11px] rounded-xl bg-blue-600 hover:bg-blue-500 text-white font-semibold shadow-sm transition-all"
@click.stop="selectCommunityInterface(iface)"
>
{{ $t("tutorial.use") }}
</button>
</div>
</div>
<div v-if="loadingInterfaces" class="flex justify-center py-3">
<v-progress-circular indeterminate color="blue" size="24"></v-progress-circular>
</div>
</div>
</div>
</div>
<div
v-if="discoveryOption !== null"
class="flex flex-col items-center gap-3 text-sm text-gray-900 dark:text-white"
>
<p class="max-w-sm text-center">
{{ $t("tutorial.custom_interfaces_desc") }}
</p>
<button
type="button"
class="px-5 py-2 text-[11px] rounded-xl border border-gray-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 text-gray-700 dark:text-zinc-300 font-semibold shadow-sm transition-all hover:bg-gray-50 dark:hover:bg-zinc-700 hover:border-blue-400 dark:hover:border-blue-500"
@click="gotoAddInterface"
>
{{ $t("tutorial.open_interfaces") }}
</button>
</div>
</div>
<!-- Step 3: Propagation Mode -->
<div v-else-if="currentStep === 3" key="step3" class="space-y-6">
<div class="text-center space-y-2">
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">
{{ $t("tutorial.propagation") }}
</h2>
<p class="text-gray-600 dark:text-zinc-400 text-base">
{{ $t("tutorial.propagation_desc") }}
</p>
</div>
<div class="flex flex-col items-center gap-6 py-4">
<div
class="bg-blue-500/10 dark:bg-blue-500/20 p-6 rounded-[2rem] text-center space-y-4 border border-blue-500/20 max-w-md"
>
<v-icon icon="mdi-server-network" color="blue" size="48"></v-icon>
<div class="text-lg font-bold text-gray-900 dark:text-white">
{{ $t("tutorial.propagation_question") }}
</div>
<p class="text-sm text-gray-600 dark:text-zinc-400">
{{ $t("tutorial.propagation_auto") }}
</p>
<div class="flex flex-col gap-3 pt-2">
<button
type="button"
class="w-full px-6 py-3 rounded-2xl bg-blue-600 hover:bg-blue-500 text-white font-bold shadow-lg transition-all transform hover:scale-[1.02]"
:disabled="savingPropagation"
@click="enableAutoPropagation"
>
<v-progress-circular
v-if="savingPropagation"
indeterminate
size="20"
width="2"
class="mr-2"
></v-progress-circular>
{{ $t("tutorial.propagation_enable_auto") }}
</button>
<button
type="button"
class="w-full px-6 py-3 rounded-2xl border-2 border-gray-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 text-gray-700 dark:text-zinc-300 font-bold shadow-sm transition-all transform hover:scale-[1.02]"
@click="nextStep"
>
{{ $t("tutorial.propagation_skip_auto") }}
</button>
</div>
<div class="mt-6 pt-6 border-t border-gray-200 dark:border-zinc-800">
<div class="text-sm font-bold text-gray-900 dark:text-white mb-1">
{{ $t("tutorial.propagation_manual") }}
</div>
<p class="text-xs text-gray-500 dark:text-zinc-500">
{{ $t("tutorial.propagation_manual_desc") }}
</p>
</div>
</div>
</div>
</div>
<!-- Step 4: Documentation & Tools -->
<div v-else-if="currentStep === 4" key="step4" class="space-y-6">
<div class="text-center space-y-2">
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">
{{ $t("tutorial.learn_create") }}
</h2>
<p class="text-gray-600 dark:text-zinc-400">
{{ $t("tutorial.learn_create_desc") }}
</p>
</div>
<div class="grid grid-cols-1 gap-4">
<div
class="flex items-start gap-4 p-4 rounded-2xl bg-gray-50 dark:bg-zinc-900 text-left border border-gray-100 dark:border-zinc-800"
>
<v-icon icon="mdi-book-open-variant" color="blue" size="32"></v-icon>
<div>
<div class="font-bold text-gray-900 dark:text-white">
{{ $t("tutorial.documentation") }}
</div>
<div class="text-sm text-gray-900 dark:text-white mb-2">
{{ $t("tutorial.documentation_desc") }}
</div>
<div class="flex gap-2">
<a
href="/meshchatx-docs/index.html"
target="_blank"
class="px-3 py-1 text-[10px] rounded-xl bg-blue-600 hover:bg-blue-500 text-white font-semibold shadow-sm transition-all inline-block"
>
{{ $t("tutorial.meshchatx_docs") }}
</a>
<a
href="/reticulum-docs/index.html"
target="_blank"
class="px-3 py-1 text-[10px] rounded-xl border border-gray-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 text-gray-700 dark:text-zinc-300 font-semibold shadow-sm transition-all hover:bg-gray-50 dark:hover:bg-zinc-700 hover:border-blue-400 dark:hover:border-blue-500 inline-block"
>
{{ $t("tutorial.reticulum_docs") }}
</a>
</div>
</div>
</div>
<div
class="flex items-start gap-4 p-4 rounded-2xl bg-gray-50 dark:bg-zinc-900 text-left border border-gray-100 dark:border-zinc-800"
>
<v-icon icon="mdi-file-document-edit-outline" color="orange" size="32"></v-icon>
<div>
<div class="font-bold text-gray-900 dark:text-white">
{{ $t("tutorial.micron_editor") }}
</div>
<div class="text-sm text-gray-900 dark:text-white mb-2">
{{ $t("tutorial.micron_editor_desc") }}
</div>
<button
type="button"
class="px-3 py-1 text-[10px] rounded-xl border border-gray-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 text-gray-700 dark:text-zinc-300 font-semibold shadow-sm transition-all hover:bg-gray-50 dark:hover:bg-zinc-700 hover:border-blue-400 dark:hover:border-blue-500"
@click="gotoRoute('micron-editor')"
>
{{ $t("tutorial.open_micron_editor") }}
</button>
</div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div
class="flex items-start gap-4 p-4 rounded-2xl bg-gray-50 dark:bg-zinc-900 text-left border border-gray-100 dark:border-zinc-800 cursor-pointer hover:border-blue-500 transition-colors"
@click="gotoRoute('nomadnetwork')"
>
<v-icon icon="mdi-earth" color="purple" size="24"></v-icon>
<div>
<div class="font-bold text-gray-900 dark:text-white text-xs">
{{ $t("tutorial.paper_messages") }}
</div>
<div class="text-[10px] text-gray-900 dark:text-white">
{{ $t("tutorial.paper_messages_desc") }}
</div>
</div>
</div>
<div
class="flex items-start gap-4 p-4 rounded-2xl bg-gray-50 dark:bg-zinc-900 text-left border border-gray-100 dark:border-zinc-800 cursor-pointer hover:border-blue-500 transition-colors"
@click="gotoRoute('messages')"
>
<v-icon icon="mdi-message-text-outline" color="green" size="24"></v-icon>
<div>
<div class="font-bold text-gray-900 dark:text-white text-xs">
{{ $t("tutorial.send_messages") }}
</div>
<div class="text-[10px] text-gray-900 dark:text-white">
{{ $t("tutorial.send_messages_desc") }}
</div>
</div>
</div>
<div
class="flex items-start gap-4 p-4 rounded-2xl bg-gray-50 dark:bg-zinc-900 text-left border border-gray-100 dark:border-zinc-800 cursor-pointer hover:border-blue-500 transition-colors"
@click="gotoRoute('network-visualiser')"
>
<v-icon icon="mdi-hub" color="teal" size="24"></v-icon>
<div>
<div class="font-bold text-gray-900 dark:text-white text-xs">
{{ $t("tutorial.explore_nodes") }}
</div>
<div class="text-[10px] text-gray-900 dark:text-white">
{{ $t("tutorial.explore_nodes_desc") }}
</div>
</div>
</div>
<div
class="flex items-start gap-4 p-4 rounded-2xl bg-gray-50 dark:bg-zinc-900 text-left border border-gray-100 dark:border-zinc-800 cursor-pointer hover:border-blue-500 transition-colors"
@click="gotoRoute('call')"
>
<v-icon icon="mdi-phone-in-talk-outline" color="red" size="24"></v-icon>
<div>
<div class="font-bold text-gray-900 dark:text-white text-xs">
{{ $t("tutorial.voice_calls") }}
</div>
<div class="text-[10px] text-gray-900 dark:text-white">
{{ $t("tutorial.voice_calls_desc") }}
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Step 5: Finish -->
<div
v-else-if="currentStep === 5"
key="step5"
class="flex flex-col items-center text-center space-y-8 py-10"
>
<div class="w-32 h-32 bg-green-500/10 rounded-full flex items-center justify-center relative">
<v-icon icon="mdi-check-decagram" color="green" size="80"></v-icon>
<div class="absolute inset-0 bg-green-500/20 rounded-full animate-ping opacity-20"></div>
</div>
<div class="space-y-3">
<h2 class="text-3xl font-black text-gray-900 dark:text-white">
{{ $t("tutorial.ready") }}
</h2>
<p class="text-lg text-gray-600 dark:text-zinc-400 max-w-md mx-auto">
{{ $t("tutorial.ready_desc") }}
</p>
</div>
<div
v-if="interfaceAddedViaTutorial"
class="p-4 bg-amber-50 dark:bg-amber-900/20 rounded-2xl border border-amber-100 dark:border-amber-900/30 text-amber-700 dark:text-amber-400 text-sm flex gap-3 max-w-md text-left"
>
<v-icon icon="mdi-information-outline" class="shrink-0"></v-icon>
<span>{{ $t("tutorial.docker_note") }}</span>
</div>
</div>
</transition>
</v-card-text>
<!-- Footer -->
<v-divider class="dark:border-zinc-900"></v-divider>
<v-card-actions class="px-6 py-6 bg-gray-50 dark:bg-zinc-950/50 flex justify-between">
<button
v-if="currentStep > 1 && currentStep < totalSteps"
type="button"
class="px-6 py-2.5 rounded-xl border border-gray-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 text-gray-700 dark:text-zinc-300 font-semibold text-sm shadow-sm transition-all hover:bg-gray-50 dark:hover:bg-zinc-700 hover:border-blue-400 dark:hover:border-blue-500"
@click="currentStep--"
>
{{ $t("tutorial.back") }}
</button>
<div v-else></div>
<div class="flex gap-3">
<button
v-if="currentStep < totalSteps"
type="button"
class="px-6 py-2.5 rounded-xl border border-gray-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 text-gray-700 dark:text-zinc-300 font-semibold text-sm shadow-sm transition-all opacity-50 hover:opacity-100 hover:bg-gray-50 dark:hover:bg-zinc-700"
@click="skipTutorial"
>
{{ $t("tutorial.skip") }}
</button>
<button
v-if="currentStep < totalSteps"
type="button"
class="px-8 h-12 rounded-xl bg-blue-600 hover:bg-blue-500 text-white font-semibold text-sm shadow-sm transition-all"
@click="nextStep"
>
{{ $t("tutorial.next") }}
</button>
<button
v-else
type="button"
class="px-8 h-12 rounded-xl bg-emerald-600 hover:bg-emerald-500 text-white font-semibold text-sm shadow-sm transition-all"
@click="finishTutorial"
>
{{ $t("tutorial.finish_setup") }}
</button>
</div>
</v-card-actions>
</v-card>
</v-dialog>
<div v-else class="flex flex-col h-full bg-white dark:bg-zinc-950 overflow-hidden relative">
<!-- Settings Controls -->
<div class="absolute top-4 left-4 z-50 flex items-center gap-1">
<LanguageSelector @language-change="onLanguageChange" />
<button
type="button"
class="rounded-full p-1.5 sm:p-2 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-zinc-800 transition-colors"
:title="config?.theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'"
@click="toggleTheme"
>
<MaterialDesignIcon
:icon-name="config?.theme === 'dark' ? 'brightness-6' : 'brightness-4'"
class="w-5 h-5 sm:w-6 sm:h-6"
/>
</button>
</div>
<!-- Progress Bar -->
<div class="w-full h-1.5 bg-gray-100 dark:bg-zinc-900 overflow-hidden flex">
<div
v-for="step in totalSteps"
:key="step"
class="h-full transition-all duration-500 ease-out"
:class="[
currentStep >= step ? 'bg-blue-500' : 'bg-transparent',
currentStep === step ? 'flex-[2]' : 'flex-1',
]"
:style="{ borderRight: step < totalSteps ? '1px solid rgba(0,0,0,0.05)' : 'none' }"
></div>
</div>
<div class="flex-1 overflow-y-auto px-6 md:px-12 py-10">
<div class="w-full h-full flex flex-col justify-between">
<transition name="fade-slide" mode="out-in">
<!-- Step 1: Welcome -->
<div
v-if="currentStep === 1"
key="page-step1"
class="flex flex-col items-center text-center space-y-8 py-10"
>
<div class="relative">
<div class="w-32 h-32 bg-blue-500/10 rounded-3xl rotate-12 absolute -inset-2"></div>
<img :src="logoUrl" class="w-32 h-32 relative z-10 p-2" />
</div>
<div class="space-y-4">
<h1 class="text-5xl font-black tracking-tight text-gray-900 dark:text-white">
{{ $t("tutorial.welcome") }} <span class="text-blue-500">MeshChatX</span>
</h1>
<p class="text-xl text-gray-600 dark:text-zinc-400 max-w-2xl mx-auto">
{{ $t("tutorial.welcome_desc") }}
</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 w-full mt-12">
<div
class="flex items-start gap-6 p-6 rounded-3xl bg-gray-50 dark:bg-zinc-900 text-left border border-gray-100 dark:border-zinc-800 transition-all hover:scale-[1.03] hover:shadow-2xl hover:z-10"
>
<v-icon icon="mdi-shield-lock" color="blue" size="40"></v-icon>
<div>
<div class="font-bold text-xl text-gray-900 dark:text-white">
{{ $t("tutorial.security") }}
</div>
<div class="text-gray-900 dark:text-white">
{{ $t("tutorial.security_desc_page") }}
</div>
</div>
</div>
<div
class="flex items-start gap-6 p-6 rounded-3xl bg-gray-50 dark:bg-zinc-900 text-left border border-gray-100 dark:border-zinc-800 transition-all hover:scale-[1.03] hover:shadow-2xl hover:z-10"
>
<v-icon icon="mdi-map-marker-path" color="purple" size="40"></v-icon>
<div>
<div class="font-bold text-xl text-gray-900 dark:text-white">
{{ $t("tutorial.maps") }}
</div>
<div class="text-gray-900 dark:text-white">
{{ $t("tutorial.maps_desc_page") }}
</div>
</div>
</div>
<div
class="flex items-start gap-6 p-6 rounded-3xl bg-gray-50 dark:bg-zinc-900 text-left border border-gray-100 dark:border-zinc-800 transition-all hover:scale-[1.03] hover:shadow-2xl hover:z-10"
>
<v-icon icon="mdi-phone" color="green" size="40"></v-icon>
<div>
<div class="font-bold text-xl text-gray-900 dark:text-white">
{{ $t("tutorial.voice") }}
</div>
<div class="text-gray-900 dark:text-white">
{{ $t("tutorial.voice_desc_page") }}
</div>
</div>
</div>
<div
class="flex items-start gap-6 p-6 rounded-3xl bg-gray-50 dark:bg-zinc-900 text-left border border-gray-100 dark:border-zinc-800 transition-all hover:scale-[1.03] hover:shadow-2xl hover:z-10"
>
<v-icon icon="mdi-tools" color="orange" size="40"></v-icon>
<div>
<div class="font-bold text-xl text-gray-900 dark:text-white">
{{ $t("tutorial.tools") }}
</div>
<div class="text-gray-900 dark:text-white">
{{ $t("tutorial.tools_desc_page") }}
</div>
</div>
</div>
<div
class="flex items-start gap-6 p-6 rounded-3xl bg-gray-50 dark:bg-zinc-900 text-left border border-gray-100 dark:border-zinc-800 transition-all hover:scale-[1.03] hover:shadow-2xl hover:z-10"
>
<v-icon icon="mdi-database-search" color="teal" size="40"></v-icon>
<div>
<div class="font-bold text-xl text-gray-900 dark:text-white">
{{ $t("tutorial.archiver") }}
</div>
<div class="text-gray-900 dark:text-white">
{{ $t("tutorial.archiver_desc_page") }}
</div>
</div>
</div>
<div
class="flex items-start gap-6 p-6 rounded-3xl bg-gray-50 dark:bg-zinc-900 text-left border border-gray-100 dark:border-zinc-800 transition-all hover:scale-[1.03] hover:shadow-2xl hover:z-10"
>
<v-icon icon="mdi-account-cancel" color="amber" size="40"></v-icon>
<div>
<div class="font-bold text-xl text-gray-900 dark:text-white">
{{ $t("tutorial.banishment") }}
</div>
<div class="text-gray-900 dark:text-white">
{{ $t("tutorial.banishment_desc") }}
</div>
</div>
</div>
<div
class="flex items-start gap-6 p-6 rounded-3xl bg-gray-50 dark:bg-zinc-900 text-left border border-gray-100 dark:border-zinc-800 transition-all hover:scale-[1.03] hover:shadow-2xl hover:z-10"
>
<v-icon icon="mdi-keyboard-outline" color="red" size="40"></v-icon>
<div>
<div class="font-bold text-xl text-gray-900 dark:text-white">
{{ $t("tutorial.palette") }}
</div>
<div class="text-gray-900 dark:text-white">
{{ $t("tutorial.palette_desc_page") }}
</div>
</div>
</div>
<div
class="flex items-start gap-6 p-6 rounded-3xl bg-gray-50 dark:bg-zinc-900 text-left border border-gray-100 dark:border-zinc-800 transition-all hover:scale-[1.03] hover:shadow-2xl hover:z-10"
>
<v-icon icon="mdi-translate" color="cyan" size="40"></v-icon>
<div>
<div class="font-bold text-xl text-gray-900 dark:text-white">
{{ $t("tutorial.i18n") }}
</div>
<div class="text-gray-900 dark:text-white">
{{ $t("tutorial.i18n_desc_page") }}
</div>
</div>
</div>
</div>
<div
class="w-full flex justify-end items-center gap-2 mt-8 px-6 text-gray-400 dark:text-zinc-500"
>
<v-icon icon="mdi-plus" size="24"></v-icon>
<span class="text-base font-bold uppercase tracking-widest">{{
$t("tutorial.more_features")
}}</span>
</div>
</div>
<!-- Step 2: Add Interface -->
<div v-else-if="currentStep === 2" key="page-step2" class="space-y-6 py-8">
<div class="text-center space-y-2">
<h2 class="text-3xl font-black text-gray-900 dark:text-white">
{{ $t("tutorial.connect") }}
</h2>
<p class="text-lg text-gray-600 dark:text-zinc-400 max-w-2xl mx-auto">
{{ $t("tutorial.connect_desc_page") }}
</p>
</div>
<div v-if="discoveryOption === null" class="flex flex-col items-center gap-8 py-12">
<div
class="bg-blue-500/10 dark:bg-blue-500/20 p-12 rounded-[3rem] text-center space-y-6 border border-blue-500/20 max-w-2xl shadow-2xl"
>
<v-icon icon="mdi-account-search" color="blue" size="80"></v-icon>
<div class="text-3xl font-black text-gray-900 dark:text-white">
{{
$t("tutorial.discovery_question") ||
"Do you want to use community interface discovering and auto-connect?"
}}
</div>
<p class="text-xl text-gray-600 dark:text-zinc-400">
{{
$t("tutorial.discovery_desc") ||
"This allows MeshChatX to automatically find and connect to public community nodes near you or on the internet."
}}
</p>
<div class="flex gap-6 justify-center pt-4">
<button
type="button"
class="px-10 py-4 text-xl rounded-2xl bg-blue-600 hover:bg-blue-500 text-white font-black shadow-xl transition-all transform hover:scale-105"
:disabled="savingDiscovery"
@click="useDiscovery"
>
<v-progress-circular
v-if="savingDiscovery"
indeterminate
size="24"
width="3"
class="mr-3"
></v-progress-circular>
{{ $t("tutorial.yes") || "Yes, use discovery" }}
</button>
<button
type="button"
class="px-10 py-4 text-xl rounded-2xl border-2 border-gray-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 text-gray-700 dark:text-zinc-300 font-black shadow-lg transition-all transform hover:scale-105"
@click="discoveryOption = 'no'"
>
{{ $t("tutorial.no") || "No, manual setup" }}
</button>
</div>
</div>
</div>
<div v-else class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-6">
<!-- Left/Middle Columns: Discovered & Community -->
<div class="lg:col-span-1 xl:col-span-2 space-y-6">
<div class="grid grid-cols-1 xl:grid-cols-2 gap-6">
<!-- Discovered Interfaces -->
<div
v-if="sortedDiscoveredInterfaces.length > 0"
class="bg-emerald-500/5 dark:bg-emerald-500/10 rounded-[1.5rem] p-5 border border-emerald-500/20 h-fit"
>
<div class="flex items-center justify-between mb-5">
<div class="flex items-center gap-2">
<v-icon icon="mdi-radar" color="emerald" size="26"></v-icon>
<span class="text-lg font-bold text-gray-900 dark:text-white"
>Discovered</span
>
</div>
<button
v-if="interfacesWithLocation.length > 0"
type="button"
class="px-2 py-1 text-[9px] rounded-lg bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 font-bold border border-emerald-500/20 hover:bg-emerald-500/20 transition-all"
@click="mapAllDiscovered"
>
Map All ({{ interfacesWithLocation.length }})
</button>
</div>
<div class="space-y-3 max-h-[600px] overflow-y-auto pr-3 custom-scrollbar">
<div
v-for="iface in sortedDiscoveredInterfaces"
:key="iface.discovery_hash || iface.name"
class="interface-card group !p-4 transition-all duration-300"
>
<div class="flex gap-4 items-start relative">
<div class="interface-card__icon !w-12 !h-12 !rounded-2xl shrink-0">
<MaterialDesignIcon
:icon-name="getDiscoveryIcon(iface)"
class="w-6 h-6"
/>
</div>
<div class="flex-1 min-w-0 space-y-2">
<div class="flex items-center gap-2 flex-nowrap min-w-0">
<div
class="text-lg font-bold text-gray-900 dark:text-white truncate min-w-0"
>
{{ iface.name }}
</div>
<span class="type-chip shrink-0">{{ iface.type }}</span>
</div>
<div class="flex items-center gap-2 flex-wrap">
<span
v-if="iface.value"
class="text-xs font-bold text-blue-600 dark:text-blue-400 shrink-0"
>
Stamps: {{ iface.value }}
</span>
</div>
<div class="flex flex-wrap gap-2 text-xs text-gray-500">
<span class="stat-chip !px-2 !py-0.5"
>Hops: {{ iface.hops }}</span
>
<span class="stat-chip !px-2 !py-0.5 capitalize shrink-0">{{
iface.status
}}</span>
<span
v-if="iface.last_heard"
class="stat-chip !px-2 !py-0.5 shrink-0"
>
{{ formatLastHeard(iface.last_heard) }}
</span>
</div>
<div
class="grid gap-1.5 pt-1 text-[11px] text-gray-500 dark:text-zinc-400 min-w-0"
>
<div
v-if="iface.reachable_on"
class="flex items-center gap-2 hover:text-blue-500 cursor-pointer transition-colors min-w-0"
@click="
copyToClipboard(
`${iface.reachable_on}:${iface.port}`,
'Address'
)
"
>
<MaterialDesignIcon
icon-name="link-variant"
class="w-4 h-4 shrink-0"
/>
<span class="truncate"
>Address: {{ iface.reachable_on }}:{{
iface.port
}}</span
>
</div>
<div
v-if="iface.transport_id"
class="flex items-center gap-2 hover:text-blue-500 cursor-pointer transition-colors min-w-0"
@click="
copyToClipboard(iface.transport_id, 'Transport ID')
"
>
<MaterialDesignIcon
icon-name="identifier"
class="w-4 h-4 shrink-0"
/>
<span class="truncate font-mono"
>Transport ID: {{ iface.transport_id }}</span
>
</div>
<div
v-if="iface.network_id"
class="flex items-center gap-2 hover:text-blue-500 cursor-pointer transition-colors min-w-0"
@click="copyToClipboard(iface.network_id, 'Network ID')"
>
<MaterialDesignIcon
icon-name="lan"
class="w-4 h-4 shrink-0"
/>
<span class="truncate font-mono"
>Network ID: {{ iface.network_id }}</span
>
</div>
<div
v-if="iface.latitude != null && iface.longitude != null"
class="flex items-center gap-2 hover:text-blue-500 cursor-pointer transition-colors min-w-0"
@click="
copyToClipboard(
`${iface.latitude}, ${iface.longitude}`,
'Location'
)
"
>
<MaterialDesignIcon
icon-name="map-marker"
class="w-4 h-4 shrink-0"
/>
<span class="truncate"
>Loc: {{ iface.latitude }},
{{ iface.longitude }}</span
>
</div>
</div>
</div>
<div class="flex items-center gap-2 shrink-0">
<span
class="text-[10px] bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 px-3 py-1 rounded-full font-bold uppercase tracking-wider"
>Heard</span
>
</div>
</div>
</div>
</div>
</div>
<!-- Community Interfaces -->
<div
class="bg-gray-50 dark:bg-zinc-900 rounded-[1.5rem] p-5 border border-gray-100 dark:border-zinc-800 h-fit"
>
<div class="flex items-center gap-2 mb-5">
<v-icon icon="mdi-web" color="blue" size="26"></v-icon>
<span class="text-lg font-bold text-gray-900 dark:text-white">{{
$t("tutorial.suggested_relays")
}}</span>
</div>
<div class="space-y-3 max-h-[600px] overflow-y-auto pr-3 custom-scrollbar">
<div
v-for="iface in communityInterfaces"
:key="iface.name"
class="flex items-center justify-between p-3 bg-white dark:bg-zinc-800 rounded-xl border border-gray-100 dark:border-zinc-700 hover:border-blue-400 transition-all cursor-pointer"
@click="selectCommunityInterface(iface)"
>
<div class="flex flex-col min-w-0">
<span
class="font-bold text-gray-900 dark:text-white text-base truncate"
>
{{ iface.name }}
</span>
<span class="text-xs text-gray-500 font-mono truncate"
>{{ iface.target_host }}:{{ iface.target_port }}</span
>
</div>
<div class="flex items-center gap-2 shrink-0">
<span
v-if="iface.online"
class="flex items-center gap-1.5 text-[9px] font-bold text-green-500 uppercase tracking-[0.2em]"
>
<span
class="w-2 h-2 rounded-full bg-green-500 animate-pulse"
></span>
{{ $t("tutorial.online") }}
</span>
<button
type="button"
class="px-4 py-1 text-[11px] rounded-xl bg-blue-600 hover:bg-blue-500 text-white font-semibold shadow-sm transition-all"
@click.stop="selectCommunityInterface(iface)"
>
{{ $t("tutorial.use") }}
</button>
</div>
</div>
<div v-if="loadingInterfaces" class="flex justify-center py-4">
<v-progress-circular
indeterminate
color="blue"
size="32"
></v-progress-circular>
</div>
</div>
</div>
</div>
</div>
<!-- Right Column: Manual Setup -->
<div
class="flex flex-col justify-center gap-4 text-sm text-gray-900 dark:text-white bg-gray-50 dark:bg-zinc-900 rounded-[1.5rem] p-8 border border-gray-100 dark:border-zinc-800 h-fit my-auto"
>
<div class="text-center space-y-4">
<v-icon icon="mdi-plus-circle-outline" size="48" color="blue"></v-icon>
<p class="text-xl font-bold text-gray-900 dark:text-white">
{{ $t("tutorial.custom_interfaces") }}
</p>
<p class="text-gray-600 dark:text-zinc-400">
{{ $t("tutorial.custom_interfaces_desc_page") }}
</p>
</div>
<button
type="button"
class="mt-4 px-6 py-3 text-sm rounded-xl border-2 border-gray-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 text-gray-700 dark:text-zinc-300 font-bold shadow-sm transition-all hover:bg-gray-50 dark:hover:bg-zinc-700 hover:border-blue-400 dark:hover:border-blue-500"
@click="gotoAddInterface"
>
{{ $t("tutorial.open_interfaces") }}
</button>
</div>
</div>
</div>
<!-- Step 3: Propagation Mode -->
<div v-else-if="currentStep === 3" key="page-step3" class="space-y-8 py-12">
<div class="text-center space-y-4">
<h2 class="text-4xl font-black text-gray-900 dark:text-white">
{{ $t("tutorial.propagation") }}
</h2>
<p class="text-xl text-gray-600 dark:text-zinc-400 max-w-2xl mx-auto">
{{ $t("tutorial.propagation_desc") }}
</p>
</div>
<div class="flex flex-col items-center gap-10 py-12">
<div
class="bg-blue-500/10 dark:bg-blue-500/20 p-12 rounded-[3rem] text-center space-y-8 border border-blue-500/20 max-w-2xl shadow-2xl"
>
<v-icon icon="mdi-server-network" color="blue" size="80"></v-icon>
<div class="text-3xl font-black text-gray-900 dark:text-white">
{{ $t("tutorial.propagation_question") }}
</div>
<p class="text-xl text-gray-600 dark:text-zinc-400">
{{ $t("tutorial.propagation_auto") }}
</p>
<div class="flex flex-col gap-4 pt-4">
<button
type="button"
class="px-10 py-4 text-xl rounded-2xl bg-blue-600 hover:bg-blue-500 text-white font-black shadow-xl transition-all transform hover:scale-105"
:disabled="savingPropagation"
@click="enableAutoPropagation"
>
<v-progress-circular
v-if="savingPropagation"
indeterminate
size="24"
width="3"
class="mr-3"
></v-progress-circular>
{{ $t("tutorial.propagation_enable_auto") }}
</button>
<button
type="button"
class="px-10 py-4 text-xl rounded-2xl border-2 border-gray-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 text-gray-700 dark:text-zinc-300 font-black shadow-lg transition-all transform hover:scale-105"
@click="nextStep"
>
{{ $t("tutorial.propagation_skip_auto") }}
</button>
</div>
<div class="mt-8 pt-8 border-t-2 border-gray-200 dark:border-zinc-800">
<div class="text-xl font-bold text-gray-900 dark:text-white mb-2">
{{ $t("tutorial.propagation_manual") }}
</div>
<p class="text-base text-gray-500 dark:text-zinc-500">
{{ $t("tutorial.propagation_manual_desc") }}
</p>
</div>
</div>
</div>
</div>
<!-- Step 4: Documentation & Tools -->
<div v-else-if="currentStep === 4" key="page-step4" class="space-y-8 py-10">
<div class="text-center space-y-4">
<h2 class="text-4xl font-black text-gray-900 dark:text-white">
{{ $t("tutorial.learn_create") }}
</h2>
<p class="text-xl text-gray-600 dark:text-zinc-400 max-w-2xl mx-auto">
{{ $t("tutorial.learn_create_desc_page") }}
</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<div
class="flex flex-col items-center gap-6 p-8 rounded-[2rem] bg-gray-50 dark:bg-zinc-900 text-center border border-gray-100 dark:border-zinc-800"
>
<v-icon icon="mdi-book-open-variant" color="blue" size="64"></v-icon>
<div>
<div class="font-bold text-2xl text-gray-900 dark:text-white mb-2">
{{ $t("tutorial.documentation") }}
</div>
<p class="text-gray-900 dark:text-white mb-6">
{{ $t("tutorial.documentation_desc_page") }}
</p>
<div class="flex flex-col gap-3">
<a
href="/meshchatx-docs/index.html"
target="_blank"
class="h-12 rounded-xl bg-blue-600 hover:bg-blue-500 text-white font-semibold shadow-sm transition-all inline-flex items-center justify-center px-6"
>
{{ $t("tutorial.read_meshchatx_docs") }}
</a>
<a
href="/reticulum-docs/index.html"
target="_blank"
class="h-12 rounded-xl border border-gray-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 text-gray-700 dark:text-zinc-300 font-semibold shadow-sm transition-all hover:bg-gray-50 dark:hover:bg-zinc-700 hover:border-blue-400 dark:hover:border-blue-500 inline-flex items-center justify-center px-6"
>
{{ $t("tutorial.reticulum_manual") }}
</a>
</div>
</div>
</div>
<div
class="flex flex-col items-center gap-6 p-8 rounded-[2rem] bg-gray-50 dark:bg-zinc-900 text-center border border-gray-100 dark:border-zinc-800"
>
<v-icon icon="mdi-file-document-edit-outline" color="orange" size="64"></v-icon>
<div>
<div class="font-bold text-2xl text-gray-900 dark:text-white mb-2">
{{ $t("tutorial.micron_editor") }}
</div>
<p class="text-gray-900 dark:text-white mb-6">
{{ $t("tutorial.micron_editor_desc_page") }}
</p>
<button
type="button"
class="w-full h-12 rounded-xl bg-orange-600 hover:bg-orange-500 text-white font-semibold shadow-sm transition-all"
@click="gotoRoute('micron-editor')"
>
{{ $t("tutorial.open_micron_editor") }}
</button>
</div>
</div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
<div
class="flex flex-col items-center gap-4 p-6 rounded-3xl bg-gray-50 dark:bg-zinc-900 text-center border border-gray-100 dark:border-zinc-800 cursor-pointer hover:border-blue-500 transition-all hover:scale-[1.02]"
@click="gotoRoute('nomadnetwork')"
>
<v-icon icon="mdi-earth" color="purple" size="40"></v-icon>
<div>
<div class="font-bold text-gray-900 dark:text-white">
{{ $t("tutorial.paper_messages") }}
</div>
<div class="text-sm text-gray-900 dark:text-white mt-1">
{{ $t("tutorial.paper_messages_desc") }}
</div>
</div>
</div>
<div
class="flex flex-col items-center gap-4 p-6 rounded-3xl bg-gray-50 dark:bg-zinc-900 text-center border border-gray-100 dark:border-zinc-800 cursor-pointer hover:border-blue-500 transition-all hover:scale-[1.02]"
@click="gotoRoute('messages')"
>
<v-icon icon="mdi-message-text-outline" color="green" size="40"></v-icon>
<div>
<div class="font-bold text-gray-900 dark:text-white">
{{ $t("tutorial.send_messages") }}
</div>
<div class="text-sm text-gray-900 dark:text-white mt-1">
{{ $t("tutorial.send_messages_desc") }}
</div>
</div>
</div>
<div
class="flex flex-col items-center gap-4 p-6 rounded-3xl bg-gray-50 dark:bg-zinc-900 text-center border border-gray-100 dark:border-zinc-800 cursor-pointer hover:border-blue-500 transition-all hover:scale-[1.02]"
@click="gotoRoute('network-visualiser')"
>
<v-icon icon="mdi-hub" color="teal" size="40"></v-icon>
<div>
<div class="font-bold text-gray-900 dark:text-white">
{{ $t("tutorial.explore_nodes") }}
</div>
<div class="text-sm text-gray-900 dark:text-white mt-1">
{{ $t("tutorial.explore_nodes_desc") }}
</div>
</div>
</div>
<div
class="flex flex-col items-center gap-4 p-6 rounded-3xl bg-gray-50 dark:bg-zinc-900 text-center border border-gray-100 dark:border-zinc-800 cursor-pointer hover:border-blue-500 transition-all hover:scale-[1.02]"
@click="gotoRoute('call')"
>
<v-icon icon="mdi-phone-in-talk-outline" color="red" size="40"></v-icon>
<div>
<div class="font-bold text-gray-900 dark:text-white">
{{ $t("tutorial.voice_calls") }}
</div>
<div class="text-sm text-gray-900 dark:text-white mt-1">
{{ $t("tutorial.voice_calls_desc") }}
</div>
</div>
</div>
</div>
</div>
<!-- Step 5: Finish -->
<div
v-else-if="currentStep === 5"
key="page-step5"
class="flex flex-col items-center text-center space-y-10 py-20"
>
<div class="w-48 h-48 bg-green-500/10 rounded-full flex items-center justify-center relative">
<v-icon icon="mdi-check-decagram" color="green" size="120"></v-icon>
<div class="absolute inset-0 bg-green-500/20 rounded-full animate-ping opacity-20"></div>
</div>
<div class="space-y-4">
<h2 class="text-5xl font-black text-gray-900 dark:text-white">
{{ $t("tutorial.ready") }}
</h2>
<p class="text-xl text-gray-600 dark:text-zinc-400 max-w-2xl mx-auto">
{{ $t("tutorial.ready_desc_page") }}
</p>
</div>
<div
class="p-6 bg-amber-50 dark:bg-amber-900/20 rounded-3xl border border-amber-100 dark:border-amber-900/30 text-amber-700 dark:text-amber-400 flex gap-4 max-w-xl text-left"
>
<v-icon icon="mdi-information-outline" size="32" class="shrink-0"></v-icon>
<div class="space-y-1">
<div class="font-bold text-lg">{{ $t("tutorial.restart_required") }}</div>
<div class="opacity-90">
{{ $t("tutorial.restart_desc_page") }}
</div>
</div>
</div>
</div>
</transition>
<!-- Navigation Buttons (Page Mode) -->
<div class="flex justify-between items-center mt-12 border-t dark:border-zinc-900 pt-8">
<button
v-if="currentStep > 1 && currentStep < totalSteps"
type="button"
class="px-8 h-12 rounded-xl border border-gray-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 text-gray-700 dark:text-zinc-300 font-semibold text-sm shadow-sm transition-all hover:bg-gray-50 dark:hover:bg-zinc-700 hover:border-blue-400 dark:hover:border-blue-500"
@click="currentStep--"
>
{{ $t("tutorial.back") }}
</button>
<div v-else></div>
<div class="flex gap-4">
<button
v-if="currentStep < totalSteps"
type="button"
class="px-8 h-12 rounded-xl border border-gray-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 text-gray-700 dark:text-zinc-300 font-semibold text-sm shadow-sm transition-all opacity-50 hover:opacity-100 hover:bg-gray-50 dark:hover:bg-zinc-700"
@click="skipTutorial"
>
{{ $t("tutorial.skip_setup") }}
</button>
<button
v-if="currentStep < totalSteps"
type="button"
class="px-12 h-14 text-lg rounded-xl bg-blue-600 hover:bg-blue-500 text-white font-semibold shadow-sm transition-all"
@click="nextStep"
>
{{ $t("tutorial.continue") }}
</button>
<button
v-else
type="button"
class="px-12 h-14 text-lg rounded-xl bg-emerald-600 hover:bg-emerald-500 text-white font-semibold shadow-sm transition-all"
@click="finishTutorial"
>
{{ $t("tutorial.finish_setup") }}
</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import logoUrl from "../assets/images/logo.png";
import ToastUtils from "../js/ToastUtils";
import DialogUtils from "../js/DialogUtils";
import ElectronUtils from "../js/ElectronUtils";
import GlobalState from "../js/GlobalState";
import LanguageSelector from "./LanguageSelector.vue";
import MaterialDesignIcon from "./MaterialDesignIcon.vue";
export default {
name: "TutorialModal",
components: {
LanguageSelector,
MaterialDesignIcon,
},
data() {
return {
visible: false,
currentStep: 1,
totalSteps: 5,
logoUrl,
communityInterfaces: [],
loadingInterfaces: false,
interfaceAddedViaTutorial: false,
discoveryOption: null,
discoveredInterfaces: [],
discoveredActive: [],
loadingDiscovered: false,
savingDiscovery: false,
savingPropagation: false,
discoveryInterval: null,
};
},
computed: {
isPage() {
return this.$route?.meta?.isPage === true;
},
isMobile() {
return window.innerWidth < 640;
},
config() {
return GlobalState.config;
},
sortedDiscoveredInterfaces() {
return [...this.discoveredInterfaces].sort((a, b) => (b.last_heard || 0) - (a.last_heard || 0));
},
interfacesWithLocation() {
return this.discoveredInterfaces.filter((iface) => iface.latitude != null && iface.longitude != null);
},
},
beforeUnmount() {
if (this.discoveryInterval) {
clearInterval(this.discoveryInterval);
}
},
mounted() {
if (this.isPage) {
this.loadCommunityInterfaces();
this.loadDiscoveredInterfaces();
this.discoveryInterval = setInterval(() => {
this.loadDiscoveredInterfaces();
}, 5000);
}
},
methods: {
async toggleTheme() {
const newTheme = this.config.theme === "dark" ? "light" : "dark";
try {
await window.axios.patch("/api/v1/config", {
theme: newTheme,
});
GlobalState.config.theme = newTheme;
} catch (e) {
console.error("Failed to update theme:", e);
}
},
async onLanguageChange(langCode) {
try {
await window.axios.patch("/api/v1/config", {
language: langCode,
});
this.$i18n.locale = langCode;
GlobalState.config.language = langCode;
} catch (e) {
console.error("Failed to update language:", e);
}
},
async show() {
this.visible = true;
this.currentStep = 1;
this.interfaceAddedViaTutorial = false;
this.discoveryOption = null;
await this.loadCommunityInterfaces();
await this.loadDiscoveredInterfaces();
if (this.discoveryInterval) {
clearInterval(this.discoveryInterval);
}
this.discoveryInterval = setInterval(() => {
this.loadDiscoveredInterfaces();
}, 5000);
},
async loadCommunityInterfaces() {
this.loadingInterfaces = true;
try {
const response = await window.axios.get("/api/v1/community-interfaces");
this.communityInterfaces = response.data.interfaces;
} catch (e) {
console.error("Failed to load community interfaces:", e);
} finally {
this.loadingInterfaces = false;
}
},
async loadDiscoveredInterfaces() {
this.loadingDiscovered = true;
try {
const response = await window.axios.get(`/api/v1/reticulum/discovered-interfaces`);
this.discoveredInterfaces = response.data?.interfaces ?? [];
this.discoveredActive = response.data?.active ?? [];
} catch (e) {
console.error("Failed to load discovered interfaces:", e);
} finally {
this.loadingDiscovered = false;
}
},
async useDiscovery() {
this.savingDiscovery = true;
try {
const payload = {
discover_interfaces: true,
autoconnect_discovered_interfaces: 3, // default to 3 slots
};
await window.axios.patch(`/api/v1/reticulum/discovery`, payload);
ToastUtils.success(this.$t("tutorial.discovery_enabled"));
this.discoveryOption = "yes";
this.nextStep();
} catch (e) {
console.error("Failed to enable discovery:", e);
ToastUtils.error(this.$t("tutorial.failed_enable_discovery"));
} finally {
this.savingDiscovery = false;
}
},
async enableAutoPropagation() {
this.savingPropagation = true;
try {
await window.axios.patch("/api/v1/config", {
lxmf_preferred_propagation_node_auto_select: true,
});
ToastUtils.success("Auto-propagation enabled");
this.nextStep();
} catch (e) {
console.error("Failed to enable auto-propagation:", e);
ToastUtils.error("Failed to enable auto-propagation");
} finally {
this.savingPropagation = false;
}
},
getDiscoveryIcon(iface) {
switch (iface.type) {
case "AutoInterface":
return "home-automation";
case "RNodeInterface":
return iface.port && iface.port.toString().startsWith("tcp://") ? "lan-connect" : "radio-tower";
case "RNodeMultiInterface":
return "access-point-network";
case "TCPClientInterface":
case "BackboneInterface":
return "lan-connect";
case "TCPServerInterface":
return "lan";
case "UDPInterface":
return "wan";
case "SerialInterface":
return "usb-port";
case "KISSInterface":
case "AX25KISSInterface":
return "antenna";
case "I2PInterface":
return "eye";
case "PipeInterface":
return "pipe";
default:
return "server-network";
}
},
formatLastHeard(ts) {
const seconds = Math.max(0, Math.floor(Date.now() / 1000 - ts));
if (seconds < 60) return `${seconds}s ago`;
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
return `${Math.floor(seconds / 86400)}d ago`;
},
copyToClipboard(text, label) {
if (!text) return;
navigator.clipboard.writeText(text);
ToastUtils.success(`${label} copied to clipboard`);
},
mapAllDiscovered() {
if (!this.isPage) {
this.visible = false;
}
this.$router.push({
name: "map",
query: { view: "discovered" },
});
},
async selectCommunityInterface(iface) {
try {
await window.axios.post("/api/v1/reticulum/interfaces/add", {
name: iface.name,
type: iface.type,
target_host: iface.target_host,
target_port: iface.target_port,
enabled: true,
});
ToastUtils.success(`Added interface: ${iface.name}`);
this.interfaceAddedViaTutorial = true;
// track change
GlobalState.hasPendingInterfaceChanges = true;
GlobalState.modifiedInterfaceNames.add(iface.name);
this.nextStep();
} catch (e) {
console.error(e);
ToastUtils.error(e.response?.data?.message || "Failed to add interface");
}
},
gotoAddInterface() {
if (!this.isPage) {
this.visible = false;
}
if (this.$router) {
this.$router.push({ path: "/interfaces/add" });
}
},
gotoRoute(routeName) {
if (!this.isPage) {
this.visible = false;
}
if (this.$router) {
this.$router.push({ name: routeName });
}
},
nextStep() {
if (this.currentStep < this.totalSteps) {
this.currentStep++;
}
},
async skipTutorial() {
if (await DialogUtils.confirm(this.$t("tutorial.skip_confirm"))) {
await this.markSeen();
this.visible = false;
}
},
async markSeen() {
try {
await window.axios.post("/api/v1/app/tutorial/seen");
} catch (e) {
console.error("Failed to mark tutorial as seen:", e);
}
},
async finishTutorial() {
await this.markSeen();
if (this.interfaceAddedViaTutorial) {
ToastUtils.info(this.$t("tutorial.ready_desc"));
}
this.visible = false;
},
async onVisibleUpdate(val) {
if (!val) {
// if closed by clicking away, mark as seen so it doesn't pop up again
await this.markSeen();
}
},
},
};
</script>
<style scoped>
.tutorial-dialog :deep(.v-field) {
border-radius: 1rem !important;
}
.tutorial-dialog :deep(.v-field--variant-outlined .v-field__outline) {
--v-field-border-opacity: 0.15;
}
.tutorial-dialog :deep(.v-field--focused .v-field__outline) {
--v-field-border-opacity: 1;
}
.tutorial-dialog :deep(.v-field__input) {
padding-top: 24px !important;
padding-bottom: 8px !important;
}
.tutorial-dialog :deep(.v-label.v-field-label--floating) {
transform: translateY(-8px) scale(0.75) !important;
font-weight: 800 !important;
text-transform: uppercase !important;
letter-spacing: 0.05em !important;
}
.tutorial-dialog :deep(.v-select .v-theme--light),
.tutorial-dialog :deep(.v-list),
.tutorial-dialog :deep(.v-list-item) {
background-color: white !important;
color: #111827 !important;
}
.tutorial-dialog :deep(.v-list-item-title),
.tutorial-dialog :deep(.v-list-item-subtitle) {
color: inherit !important;
}
.tutorial-dialog :deep(.dark .v-list),
.tutorial-dialog :deep(.dark .v-list-item) {
background-color: #18181b !important;
color: white !important;
}
.tutorial-dialog :deep(.v-field__input) {
color: inherit !important;
}
.tutorial-dialog :deep(.v-label.v-field-label) {
color: #6b7280 !important;
}
.tutorial-dialog :deep(.dark .v-label.v-field-label) {
color: #a1a1aa !important;
}
.tutorial-dialog .v-overlay__content {
border-radius: 2rem !important;
overflow: hidden;
}
.fade-slide-enter-active,
.fade-slide-leave-active {
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
.fade-slide-enter-from {
opacity: 0;
transform: translateX(30px);
}
.fade-slide-leave-to {
opacity: 0;
transform: translateX(-30px);
}
</style>