mirror of
https://git.quad4.io/RNS-Things/MeshChatX.git
synced 2026-08-28 05:24:45 +00:00
feat(docs): update documentation navigation and add bundled documentation hints in interface components
This commit is contained in:
@@ -1212,6 +1212,24 @@ export default {
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (json.ingest_type === "docs_view") {
|
||||
const dq = json.docs_query;
|
||||
const rel = dq && typeof dq.reticulum === "string" ? dq.reticulum.trim() : "";
|
||||
if (rel) {
|
||||
await this.$router.push({
|
||||
name: "documentation",
|
||||
query: { reticulum: encodeURIComponent(rel) },
|
||||
});
|
||||
} else {
|
||||
await this.$router.push({ name: "documentation" });
|
||||
}
|
||||
if (json.status === "error") {
|
||||
ToastUtils.error(json.message);
|
||||
} else if (json.message) {
|
||||
ToastUtils.info(json.message);
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (json.status === "success") {
|
||||
ToastUtils.success(json.message);
|
||||
} else if (json.status === "error") {
|
||||
@@ -1766,6 +1784,33 @@ export default {
|
||||
this.$router.push({ name: "call", query: { tab: "phone" } });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const u = new URL(normalizedUrl);
|
||||
const proto = u.protocol.toLowerCase();
|
||||
const host = u.hostname.toLowerCase();
|
||||
if ((proto === "meshchatx:" || proto === "meshchat:") && host === "docs") {
|
||||
let rel = u.searchParams.get("reticulum") ?? u.searchParams.get("path") ?? "";
|
||||
rel = String(rel).trim();
|
||||
if (!rel && u.pathname && u.pathname !== "/") {
|
||||
try {
|
||||
rel = decodeURIComponent(u.pathname.replace(/^\/+/, ""));
|
||||
} catch {
|
||||
rel = u.pathname.replace(/^\/+/, "");
|
||||
}
|
||||
}
|
||||
if (rel) {
|
||||
this.$router.push({
|
||||
name: "documentation",
|
||||
query: { reticulum: encodeURIComponent(rel) },
|
||||
});
|
||||
} else {
|
||||
this.$router.push({ name: "documentation" });
|
||||
}
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
/* not a valid URL; continue */
|
||||
}
|
||||
if (/^(meshchatx|meshchat):\/\/map\b/i.test(normalizedUrl)) {
|
||||
WebSocketConnection.send(
|
||||
JSON.stringify({
|
||||
|
||||
@@ -280,6 +280,41 @@
|
||||
<p class="text-gray-600 dark:text-zinc-400 text-sm">
|
||||
{{ $t("tutorial.bootstrap_desc") }}
|
||||
</p>
|
||||
<div class="flex flex-col items-center gap-2 pt-1">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center gap-2 rounded-xl border border-blue-500/30 bg-blue-500/10 px-4 py-2 text-xs font-semibold text-blue-700 transition-colors hover:bg-blue-500/15 dark:text-blue-300 dark:hover:bg-blue-500/20 disabled:opacity-60"
|
||||
:disabled="loadingInterfaces || loadingDiscovered || pickingRandomBootstraps"
|
||||
@click="pickRandomTcpBootstraps"
|
||||
>
|
||||
<v-progress-circular
|
||||
v-if="pickingRandomBootstraps"
|
||||
indeterminate
|
||||
size="16"
|
||||
width="2"
|
||||
/>
|
||||
<v-icon v-else icon="mdi-shuffle-variant" size="18" />
|
||||
{{ $t("tutorial.bootstrap_pick_random_tcp") }}
|
||||
</button>
|
||||
<div
|
||||
v-if="bootstrapSelectedLabels.length > 0"
|
||||
class="w-full max-w-md rounded-xl border border-gray-200/90 bg-gray-50/80 px-3 py-2 text-left dark:border-zinc-700 dark:bg-zinc-900/50"
|
||||
>
|
||||
<div
|
||||
class="text-[10px] font-bold uppercase tracking-wide text-gray-500 dark:text-zinc-400"
|
||||
>
|
||||
{{ $t("tutorial.bootstrap_selected_nodes_heading") }}
|
||||
</div>
|
||||
<ul class="mt-1 space-y-0.5 text-xs text-gray-800 dark:text-zinc-200">
|
||||
<li
|
||||
v-for="(label, idx) in bootstrapSelectedLabels"
|
||||
:key="selectedBootstrapKeys[idx]"
|
||||
>
|
||||
{{ label }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -594,7 +629,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 5: Documentation & Tools -->
|
||||
<!-- Step 5: Learn & Create -->
|
||||
<div v-else-if="currentStep === 5" key="step5-tools" class="space-y-6">
|
||||
<div class="text-center space-y-2">
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
@@ -605,114 +640,197 @@
|
||||
</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-xs transition-all inline-block"
|
||||
>
|
||||
{{ $t("tutorial.meshchatx_docs") }}
|
||||
</a>
|
||||
<a
|
||||
:href="reticulumBundledDocsUrl"
|
||||
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-xs 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-xs 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="space-y-6">
|
||||
<div class="flex w-full flex-col gap-4 max-w-xl mx-auto">
|
||||
<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')"
|
||||
class="flex w-full 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 touch-manipulation"
|
||||
>
|
||||
<v-icon icon="mdi-earth" color="purple" size="24"></v-icon>
|
||||
<v-icon icon="mdi-book-open-variant" color="blue" size="32"></v-icon>
|
||||
<div class="min-w-0 flex-1">
|
||||
<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 flex-wrap 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-xs transition-all inline-block"
|
||||
>
|
||||
{{ $t("tutorial.meshchatx_docs") }}
|
||||
</a>
|
||||
<a
|
||||
:href="reticulumBundledDocsUrl"
|
||||
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-xs 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 w-full 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 touch-manipulation"
|
||||
>
|
||||
<v-icon icon="mdi-file-document-edit-outline" color="orange" size="32"></v-icon>
|
||||
<div class="min-w-0 flex-1">
|
||||
<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>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<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-xs 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>
|
||||
<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-xs transition-all hover:bg-gray-50 dark:hover:bg-zinc-700 hover:border-blue-400 dark:hover:border-blue-500"
|
||||
@click="gotoRoute('mesh-server')"
|
||||
>
|
||||
{{ $t("tutorial.open_mesh_server") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex w-full cursor-pointer 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-colors hover:border-indigo-500 touch-manipulation min-h-[4.5rem]"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="gotoRoute('identities')"
|
||||
@keydown.enter="gotoRoute('identities')"
|
||||
>
|
||||
<v-icon icon="mdi-account-multiple-outline" color="indigo" size="32"></v-icon>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="font-bold text-gray-900 dark:text-white">
|
||||
{{ $t("tutorial.identities_card_title") }}
|
||||
</div>
|
||||
<div class="text-sm text-gray-600 dark:text-zinc-400">
|
||||
{{ $t("tutorial.identities_card_desc") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex w-full cursor-pointer 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-colors hover:border-teal-500 touch-manipulation min-h-[4.5rem]"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="gotoRoute('archives')"
|
||||
@keydown.enter="gotoRoute('archives')"
|
||||
>
|
||||
<v-icon icon="mdi-archive-outline" color="teal" size="32"></v-icon>
|
||||
<div>
|
||||
<div class="font-bold text-gray-900 dark:text-white text-xs">
|
||||
<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>
|
||||
|
||||
<p
|
||||
class="text-center text-[11px] font-semibold tracking-wide text-gray-600 dark:text-zinc-400 px-2"
|
||||
>
|
||||
{{ $t("tutorial.learn_create_more") }}
|
||||
</p>
|
||||
|
||||
<div class="grid grid-cols-2 gap-2 max-w-xl mx-auto">
|
||||
<div
|
||||
class="flex flex-col gap-1.5 rounded-xl border border-gray-100 bg-gray-50 p-2.5 dark:border-zinc-800 dark:bg-zinc-900 cursor-pointer hover:border-blue-500 transition-colors touch-manipulation min-h-[5.5rem]"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="gotoRoute('nomadnetwork')"
|
||||
@keydown.enter="gotoRoute('nomadnetwork')"
|
||||
>
|
||||
<v-icon icon="mdi-earth" color="purple" size="22" class="shrink-0"></v-icon>
|
||||
<div class="min-w-0">
|
||||
<div class="font-bold text-gray-900 dark:text-white text-[11px] leading-tight">
|
||||
{{ $t("tutorial.paper_messages") }}
|
||||
</div>
|
||||
<div class="text-[10px] text-gray-900 dark:text-white">
|
||||
<div
|
||||
class="text-[9px] text-gray-600 dark:text-zinc-400 leading-snug line-clamp-3"
|
||||
>
|
||||
{{ $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"
|
||||
class="flex flex-col gap-1.5 rounded-xl border border-gray-100 bg-gray-50 p-2.5 dark:border-zinc-800 dark:bg-zinc-900 cursor-pointer hover:border-blue-500 transition-colors touch-manipulation min-h-[5.5rem]"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="gotoRoute('messages')"
|
||||
@keydown.enter="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">
|
||||
<v-icon
|
||||
icon="mdi-message-text-outline"
|
||||
color="green"
|
||||
size="22"
|
||||
class="shrink-0"
|
||||
></v-icon>
|
||||
<div class="min-w-0">
|
||||
<div class="font-bold text-gray-900 dark:text-white text-[11px] leading-tight">
|
||||
{{ $t("tutorial.send_messages") }}
|
||||
</div>
|
||||
<div class="text-[10px] text-gray-900 dark:text-white">
|
||||
<div
|
||||
class="text-[9px] text-gray-600 dark:text-zinc-400 leading-snug line-clamp-3"
|
||||
>
|
||||
{{ $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"
|
||||
class="flex flex-col gap-1.5 rounded-xl border border-gray-100 bg-gray-50 p-2.5 dark:border-zinc-800 dark:bg-zinc-900 cursor-pointer hover:border-blue-500 transition-colors touch-manipulation min-h-[5.5rem]"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="gotoRoute('network-visualiser')"
|
||||
@keydown.enter="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">
|
||||
<v-icon icon="mdi-hub" color="teal" size="22" class="shrink-0"></v-icon>
|
||||
<div class="min-w-0">
|
||||
<div class="font-bold text-gray-900 dark:text-white text-[11px] leading-tight">
|
||||
{{ $t("tutorial.explore_nodes") }}
|
||||
</div>
|
||||
<div class="text-[10px] text-gray-900 dark:text-white">
|
||||
<div
|
||||
class="text-[9px] text-gray-600 dark:text-zinc-400 leading-snug line-clamp-3"
|
||||
>
|
||||
{{ $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"
|
||||
class="flex flex-col gap-1.5 rounded-xl border border-gray-100 bg-gray-50 p-2.5 dark:border-zinc-800 dark:bg-zinc-900 cursor-pointer hover:border-blue-500 transition-colors touch-manipulation min-h-[5.5rem]"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="gotoRoute('call')"
|
||||
@keydown.enter="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">
|
||||
<v-icon
|
||||
icon="mdi-phone-in-talk-outline"
|
||||
color="red"
|
||||
size="22"
|
||||
class="shrink-0"
|
||||
></v-icon>
|
||||
<div class="min-w-0">
|
||||
<div class="font-bold text-gray-900 dark:text-white text-[11px] leading-tight">
|
||||
{{ $t("tutorial.voice_calls") }}
|
||||
</div>
|
||||
<div class="text-[10px] text-gray-900 dark:text-white">
|
||||
<div
|
||||
class="text-[9px] text-gray-600 dark:text-zinc-400 leading-snug line-clamp-3"
|
||||
>
|
||||
{{ $t("tutorial.voice_calls_desc") }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1063,6 +1181,41 @@
|
||||
<p class="text-lg text-gray-600 dark:text-zinc-400 max-w-3xl mx-auto">
|
||||
{{ $t("tutorial.bootstrap_desc_page") }}
|
||||
</p>
|
||||
<div class="flex flex-col items-center gap-3 pt-2">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center gap-2 rounded-xl border-2 border-blue-500/30 bg-blue-500/10 px-5 py-2.5 text-sm font-semibold text-blue-700 transition-colors hover:bg-blue-500/15 dark:text-blue-300 dark:hover:bg-blue-500/20 disabled:opacity-60"
|
||||
:disabled="loadingInterfaces || loadingDiscovered || pickingRandomBootstraps"
|
||||
@click="pickRandomTcpBootstraps"
|
||||
>
|
||||
<v-progress-circular
|
||||
v-if="pickingRandomBootstraps"
|
||||
indeterminate
|
||||
size="18"
|
||||
width="2"
|
||||
/>
|
||||
<v-icon v-else icon="mdi-shuffle-variant" size="20" />
|
||||
{{ $t("tutorial.bootstrap_pick_random_tcp") }}
|
||||
</button>
|
||||
<div
|
||||
v-if="bootstrapSelectedLabels.length > 0"
|
||||
class="w-full max-w-xl rounded-xl border border-gray-200/90 bg-gray-50/80 px-4 py-3 text-left dark:border-zinc-700 dark:bg-zinc-900/50"
|
||||
>
|
||||
<div
|
||||
class="text-xs font-bold uppercase tracking-wide text-gray-500 dark:text-zinc-400"
|
||||
>
|
||||
{{ $t("tutorial.bootstrap_selected_nodes_heading") }}
|
||||
</div>
|
||||
<ul class="mt-1.5 space-y-1 text-sm text-gray-800 dark:text-zinc-200">
|
||||
<li
|
||||
v-for="(label, idx) in bootstrapSelectedLabels"
|
||||
:key="selectedBootstrapKeys[idx]"
|
||||
>
|
||||
{{ label }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -1382,7 +1535,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 5: Documentation & Tools -->
|
||||
<!-- Step 5: Learn & Create -->
|
||||
<div v-else-if="currentStep === 5" key="page-step5-tools" 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">
|
||||
@@ -1393,116 +1546,243 @@
|
||||
</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-4xl 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 class="space-y-8 px-2 sm:px-0">
|
||||
<div class="flex w-full flex-col gap-5 max-w-2xl mx-auto">
|
||||
<div
|
||||
class="flex w-full flex-col gap-4 rounded-3xl border border-gray-100 bg-gray-50 p-6 dark:border-zinc-800 dark:bg-zinc-900 sm:p-8 sm:rounded-[2rem] touch-manipulation"
|
||||
>
|
||||
<div class="flex gap-4 sm:gap-5">
|
||||
<v-icon
|
||||
icon="mdi-book-open-variant"
|
||||
color="blue"
|
||||
size="56"
|
||||
class="shrink-0"
|
||||
></v-icon>
|
||||
<div class="min-w-0 flex-1 text-left">
|
||||
<div
|
||||
class="text-xl font-bold text-gray-900 dark:text-white sm:text-2xl mb-2"
|
||||
>
|
||||
{{ $t("tutorial.documentation") }}
|
||||
</div>
|
||||
<p class="text-gray-700 dark:text-zinc-300 mb-6 text-base">
|
||||
{{ $t("tutorial.documentation_desc_page") }}
|
||||
</p>
|
||||
<div class="flex flex-col gap-3">
|
||||
<a
|
||||
href="/meshchatx-docs/index.html"
|
||||
target="_blank"
|
||||
class="flex min-h-12 items-center justify-center rounded-xl bg-blue-600 px-4 py-3 text-base font-semibold text-white shadow-xs transition-all hover:bg-blue-500"
|
||||
>
|
||||
{{ $t("tutorial.read_meshchatx_docs") }}
|
||||
</a>
|
||||
<a
|
||||
:href="reticulumBundledDocsUrl"
|
||||
target="_blank"
|
||||
class="flex min-h-12 items-center justify-center rounded-xl border border-gray-300 bg-white px-4 py-3 text-base font-semibold text-gray-700 shadow-xs transition-all hover:border-blue-400 hover:bg-gray-50 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-300 dark:hover:border-blue-500 dark:hover:bg-zinc-700"
|
||||
>
|
||||
{{ $t("tutorial.reticulum_manual") }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</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-xs transition-all inline-flex items-center justify-center px-6"
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex w-full flex-col gap-4 rounded-3xl border border-gray-100 bg-gray-50 p-6 dark:border-zinc-800 dark:bg-zinc-900 sm:p-8 sm:rounded-[2rem] touch-manipulation"
|
||||
>
|
||||
<div class="flex gap-4 sm:gap-5">
|
||||
<v-icon
|
||||
icon="mdi-file-document-edit-outline"
|
||||
color="orange"
|
||||
size="56"
|
||||
class="shrink-0"
|
||||
></v-icon>
|
||||
<div class="min-w-0 flex-1 text-left">
|
||||
<div
|
||||
class="text-xl font-bold text-gray-900 dark:text-white sm:text-2xl mb-2"
|
||||
>
|
||||
{{ $t("tutorial.micron_editor") }}
|
||||
</div>
|
||||
<p class="text-gray-700 dark:text-zinc-300 mb-6 text-base">
|
||||
{{ $t("tutorial.micron_editor_desc_page") }}
|
||||
</p>
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex flex-col gap-3 sm:flex-row">
|
||||
<button
|
||||
type="button"
|
||||
class="flex min-h-12 flex-1 items-center justify-center rounded-xl bg-orange-600 px-4 py-3 text-base font-semibold text-white transition-all hover:bg-orange-500"
|
||||
@click="gotoRoute('micron-editor')"
|
||||
>
|
||||
{{ $t("tutorial.open_micron_editor") }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="flex min-h-12 flex-1 items-center justify-center rounded-xl border border-gray-300 bg-white px-4 py-3 text-base font-semibold text-gray-700 transition-all hover:bg-gray-50 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-300 dark:hover:bg-zinc-700"
|
||||
@click="gotoRoute('mesh-server')"
|
||||
>
|
||||
{{ $t("tutorial.open_mesh_server") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex w-full cursor-pointer flex-col gap-4 rounded-3xl border border-gray-100 bg-gray-50 p-6 transition-colors hover:border-indigo-500 dark:border-zinc-800 dark:bg-zinc-900 sm:p-8 sm:rounded-[2rem] touch-manipulation"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="gotoRoute('identities')"
|
||||
@keydown.enter="gotoRoute('identities')"
|
||||
>
|
||||
<div class="flex gap-4 sm:gap-5">
|
||||
<v-icon
|
||||
icon="mdi-account-multiple-outline"
|
||||
color="indigo"
|
||||
size="56"
|
||||
class="shrink-0"
|
||||
></v-icon>
|
||||
<div class="min-w-0 flex-1 text-left">
|
||||
<div
|
||||
class="text-xl font-bold text-gray-900 dark:text-white sm:text-2xl mb-2"
|
||||
>
|
||||
{{ $t("tutorial.identities_card_title") }}
|
||||
</div>
|
||||
<p class="text-gray-700 dark:text-zinc-300 text-base">
|
||||
{{ $t("tutorial.identities_card_desc_page") }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex w-full cursor-pointer flex-col gap-4 rounded-3xl border border-gray-100 bg-gray-50 p-6 transition-colors hover:border-teal-500 dark:border-zinc-800 dark:bg-zinc-900 sm:p-8 sm:rounded-[2rem] touch-manipulation min-h-[5rem]"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="gotoRoute('archives')"
|
||||
@keydown.enter="gotoRoute('archives')"
|
||||
>
|
||||
<div class="flex gap-4 sm:gap-5">
|
||||
<v-icon
|
||||
icon="mdi-archive-outline"
|
||||
color="teal"
|
||||
size="56"
|
||||
class="shrink-0"
|
||||
></v-icon>
|
||||
<div class="min-w-0 flex-1 text-left">
|
||||
<div
|
||||
class="text-xl font-bold text-gray-900 dark:text-white sm:text-2xl mb-2"
|
||||
>
|
||||
{{ $t("tutorial.archiver") }}
|
||||
</div>
|
||||
<p class="text-gray-700 dark:text-zinc-300 text-base">
|
||||
{{ $t("tutorial.archiver_desc_page") }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-center text-sm font-semibold text-gray-600 dark:text-zinc-400 px-2">
|
||||
{{ $t("tutorial.learn_create_more") }}
|
||||
</p>
|
||||
|
||||
<div class="grid grid-cols-2 gap-3 max-w-2xl mx-auto px-1 sm:px-0">
|
||||
<div
|
||||
class="flex cursor-pointer flex-col gap-2 rounded-2xl border border-gray-100 bg-gray-50 p-3 dark:border-zinc-800 dark:bg-zinc-900 transition-colors hover:border-blue-500 touch-manipulation min-h-[6.5rem]"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="gotoRoute('nomadnetwork')"
|
||||
@keydown.enter="gotoRoute('nomadnetwork')"
|
||||
>
|
||||
<v-icon icon="mdi-earth" color="purple" size="28" class="shrink-0"></v-icon>
|
||||
<div class="min-w-0">
|
||||
<div
|
||||
class="font-bold text-gray-900 dark:text-white text-xs sm:text-sm leading-tight"
|
||||
>
|
||||
{{ $t("tutorial.read_meshchatx_docs") }}
|
||||
</a>
|
||||
<a
|
||||
:href="reticulumBundledDocsUrl"
|
||||
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-xs 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.paper_messages") }}
|
||||
</div>
|
||||
<div
|
||||
class="text-[10px] sm:text-xs text-gray-600 dark:text-zinc-400 mt-1 leading-snug line-clamp-4"
|
||||
>
|
||||
{{ $t("tutorial.reticulum_manual") }}
|
||||
</a>
|
||||
{{ $t("tutorial.paper_messages_desc") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex flex-col items-center gap-6 p-8 rounded-4xl 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-xs 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
|
||||
class="flex cursor-pointer flex-col gap-2 rounded-2xl border border-gray-100 bg-gray-50 p-3 dark:border-zinc-800 dark:bg-zinc-900 transition-colors hover:border-blue-500 touch-manipulation min-h-[6.5rem]"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="gotoRoute('messages')"
|
||||
@keydown.enter="gotoRoute('messages')"
|
||||
>
|
||||
<v-icon
|
||||
icon="mdi-message-text-outline"
|
||||
color="green"
|
||||
size="28"
|
||||
class="shrink-0"
|
||||
></v-icon>
|
||||
<div class="min-w-0">
|
||||
<div
|
||||
class="font-bold text-gray-900 dark:text-white text-xs sm:text-sm leading-tight"
|
||||
>
|
||||
{{ $t("tutorial.send_messages") }}
|
||||
</div>
|
||||
<div
|
||||
class="text-[10px] sm:text-xs text-gray-600 dark:text-zinc-400 mt-1 leading-snug line-clamp-4"
|
||||
>
|
||||
{{ $t("tutorial.send_messages_desc") }}
|
||||
</div>
|
||||
</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
|
||||
class="flex cursor-pointer flex-col gap-2 rounded-2xl border border-gray-100 bg-gray-50 p-3 dark:border-zinc-800 dark:bg-zinc-900 transition-colors hover:border-blue-500 touch-manipulation min-h-[6.5rem]"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="gotoRoute('network-visualiser')"
|
||||
@keydown.enter="gotoRoute('network-visualiser')"
|
||||
>
|
||||
<v-icon icon="mdi-hub" color="teal" size="28" class="shrink-0"></v-icon>
|
||||
<div class="min-w-0">
|
||||
<div
|
||||
class="font-bold text-gray-900 dark:text-white text-xs sm:text-sm leading-tight"
|
||||
>
|
||||
{{ $t("tutorial.explore_nodes") }}
|
||||
</div>
|
||||
<div
|
||||
class="text-[10px] sm:text-xs text-gray-600 dark:text-zinc-400 mt-1 leading-snug line-clamp-4"
|
||||
>
|
||||
{{ $t("tutorial.explore_nodes_desc") }}
|
||||
</div>
|
||||
</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
|
||||
class="flex cursor-pointer flex-col gap-2 rounded-2xl border border-gray-100 bg-gray-50 p-3 dark:border-zinc-800 dark:bg-zinc-900 transition-colors hover:border-blue-500 touch-manipulation min-h-[6.5rem]"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="gotoRoute('call')"
|
||||
@keydown.enter="gotoRoute('call')"
|
||||
>
|
||||
<v-icon
|
||||
icon="mdi-phone-in-talk-outline"
|
||||
color="red"
|
||||
size="28"
|
||||
class="shrink-0"
|
||||
></v-icon>
|
||||
<div class="min-w-0">
|
||||
<div
|
||||
class="font-bold text-gray-900 dark:text-white text-xs sm:text-sm leading-tight"
|
||||
>
|
||||
{{ $t("tutorial.voice_calls") }}
|
||||
</div>
|
||||
<div
|
||||
class="text-[10px] sm:text-xs text-gray-600 dark:text-zinc-400 mt-1 leading-snug line-clamp-4"
|
||||
>
|
||||
{{ $t("tutorial.voice_calls_desc") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1632,6 +1912,8 @@ export default {
|
||||
bootstrapListSearch: "",
|
||||
bootstrapDiscoveredSectionOpen: true,
|
||||
bootstrapCommunitySectionOpen: true,
|
||||
bootstrapAutoPickDone: false,
|
||||
pickingRandomBootstraps: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -1696,6 +1978,22 @@ export default {
|
||||
reticulumBundledDocsUrl() {
|
||||
return bundledReticulumDocsUrl(this.$i18n.locale);
|
||||
},
|
||||
bootstrapSelectedLabels() {
|
||||
return this.selectedBootstrapKeys.map((k) => this.bootstrapDisplayLabelForKey(k)).filter(Boolean);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
communityInterfaces() {
|
||||
this.$nextTick(() => void this.maybeAutoPickBootstrapTcp());
|
||||
},
|
||||
discoveredInterfaces() {
|
||||
this.$nextTick(() => void this.maybeAutoPickBootstrapTcp());
|
||||
},
|
||||
currentStep(val) {
|
||||
if (val === 3) {
|
||||
this.$nextTick(() => void this.maybeAutoPickBootstrapTcp());
|
||||
}
|
||||
},
|
||||
},
|
||||
beforeUnmount() {
|
||||
if (this.onWindowResize) {
|
||||
@@ -1752,6 +2050,7 @@ export default {
|
||||
this.bootstrapListSearch = "";
|
||||
this.bootstrapDiscoveredSectionOpen = true;
|
||||
this.bootstrapCommunitySectionOpen = true;
|
||||
this.bootstrapAutoPickDone = false;
|
||||
await this.loadDiscoveryBootstrapDefaults();
|
||||
await this.loadCommunityInterfaces();
|
||||
await this.loadDiscoveredInterfaces();
|
||||
@@ -1834,6 +2133,9 @@ export default {
|
||||
this.bootstrapListSearch = "";
|
||||
this.bootstrapDiscoveredSectionOpen = true;
|
||||
this.bootstrapCommunitySectionOpen = true;
|
||||
await this.loadCommunityInterfaces();
|
||||
await this.loadDiscoveredInterfaces();
|
||||
await this.maybeAutoPickBootstrapTcp();
|
||||
} catch (e) {
|
||||
console.error("Failed to enable discovery:", e);
|
||||
ToastUtils.error(this.$t("tutorial.failed_enable_discovery"));
|
||||
@@ -1879,6 +2181,176 @@ export default {
|
||||
this.selectedBootstrapKeys.push(key);
|
||||
}
|
||||
},
|
||||
bootstrapDisplayLabelForKey(key) {
|
||||
if (!key) {
|
||||
return "";
|
||||
}
|
||||
if (key.startsWith("comm:")) {
|
||||
const name = key.slice(5);
|
||||
const iface = this.communityInterfaces.find((c) => c.name === name);
|
||||
return iface?.name || name;
|
||||
}
|
||||
if (key.startsWith("disc:")) {
|
||||
const suffix = key.slice(5);
|
||||
const iface = this.discoveredInterfaces.find((d) => String(d.discovery_hash || d.name) === suffix);
|
||||
return iface?.name || suffix;
|
||||
}
|
||||
return key;
|
||||
},
|
||||
communityBootstrapExcludedFromRandom(iface) {
|
||||
const name = String(iface.name || "");
|
||||
const desc = String(iface.description || "");
|
||||
const host = String(iface.target_host || "").trim();
|
||||
const hay = `${name} ${desc}`.toLowerCase();
|
||||
if (hay.includes("yggdrasil")) {
|
||||
return true;
|
||||
}
|
||||
if (/\bygg\b/.test(hay) || hay.includes("-ygg") || hay.includes(" ygg") || hay.includes("(ygg")) {
|
||||
return true;
|
||||
}
|
||||
if (/^(200|201|202|203):[0-9a-f:]+$/i.test(host)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
pickEligibleCommunityTcpBootstrapForRandom() {
|
||||
const out = [];
|
||||
for (const iface of this.communityInterfaces) {
|
||||
const t = iface.type;
|
||||
if (t !== "TCPClientInterface" && t !== "BackboneInterface") {
|
||||
continue;
|
||||
}
|
||||
const host = String(iface.target_host || "").trim();
|
||||
const port = iface.target_port;
|
||||
if (!host || port === undefined || port === null || port === "") {
|
||||
continue;
|
||||
}
|
||||
if (this.communityBootstrapExcludedFromRandom(iface)) {
|
||||
continue;
|
||||
}
|
||||
out.push({
|
||||
key: `comm:${iface.name}`,
|
||||
kind: "community",
|
||||
iface,
|
||||
dedupe: `${host.toLowerCase()}:${Number(port)}`,
|
||||
});
|
||||
}
|
||||
return out;
|
||||
},
|
||||
pickEligibleTcpBootstrapEntries() {
|
||||
const out = [];
|
||||
for (const iface of this.communityInterfaces) {
|
||||
const t = iface.type;
|
||||
if (t !== "TCPClientInterface" && t !== "BackboneInterface") {
|
||||
continue;
|
||||
}
|
||||
const host = String(iface.target_host || "").trim();
|
||||
const port = iface.target_port;
|
||||
if (!host || port === undefined || port === null || port === "") {
|
||||
continue;
|
||||
}
|
||||
out.push({
|
||||
key: `comm:${iface.name}`,
|
||||
kind: "community",
|
||||
iface,
|
||||
dedupe: `${host.toLowerCase()}:${Number(port)}`,
|
||||
});
|
||||
}
|
||||
for (const iface of this.discoveredInterfaces) {
|
||||
const host = String(iface.reachable_on || "").trim();
|
||||
const port = iface.port;
|
||||
if (!host || port === undefined || port === null || port === "") {
|
||||
continue;
|
||||
}
|
||||
const typ = iface.type || "";
|
||||
if (typ && typ !== "BackboneInterface" && typ !== "TCPClientInterface") {
|
||||
continue;
|
||||
}
|
||||
out.push({
|
||||
key: `disc:${iface.discovery_hash || iface.name}`,
|
||||
kind: "discovered",
|
||||
iface,
|
||||
dedupe: `${host.toLowerCase()}:${Number(port)}`,
|
||||
});
|
||||
}
|
||||
return out;
|
||||
},
|
||||
dedupeBootstrapEntries(entries) {
|
||||
const seen = new Set();
|
||||
const deduped = [];
|
||||
for (const e of entries) {
|
||||
if (seen.has(e.dedupe)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(e.dedupe);
|
||||
deduped.push(e);
|
||||
}
|
||||
return deduped;
|
||||
},
|
||||
shuffleArrayInPlace(arr) {
|
||||
for (let i = arr.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[arr[i], arr[j]] = [arr[j], arr[i]];
|
||||
}
|
||||
},
|
||||
async pickRandomTcpBootstraps(options = {}) {
|
||||
const silent = options.silent === true;
|
||||
const auto = options.auto === true;
|
||||
if (!silent && !auto) {
|
||||
this.pickingRandomBootstraps = true;
|
||||
}
|
||||
await Promise.resolve();
|
||||
await new Promise((resolve) => {
|
||||
if (typeof requestAnimationFrame !== "undefined") {
|
||||
requestAnimationFrame(() => resolve());
|
||||
} else {
|
||||
setTimeout(resolve, 0);
|
||||
}
|
||||
});
|
||||
try {
|
||||
let entries = this.pickEligibleCommunityTcpBootstrapForRandom();
|
||||
entries = this.dedupeBootstrapEntries(entries);
|
||||
if (entries.length === 0) {
|
||||
if (!silent && !auto) {
|
||||
ToastUtils.warning(this.$t("tutorial.bootstrap_pick_random_none"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.shuffleArrayInPlace(entries);
|
||||
const take = Math.min(3, entries.length);
|
||||
this.selectedBootstrapKeys = entries.slice(0, take).map((e) => e.key);
|
||||
const labels = this.selectedBootstrapKeys.map((k) => this.bootstrapDisplayLabelForKey(k));
|
||||
if (!silent && !auto) {
|
||||
ToastUtils.success(
|
||||
this.$t("tutorial.bootstrap_pick_random_done", {
|
||||
count: take,
|
||||
names: labels.join(", "),
|
||||
})
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (!silent && !auto) {
|
||||
this.pickingRandomBootstraps = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
async maybeAutoPickBootstrapTcp() {
|
||||
if (this.bootstrapAutoPickDone) {
|
||||
return;
|
||||
}
|
||||
if (this.currentStep !== 3 || this.connectionMode !== "discovery") {
|
||||
return;
|
||||
}
|
||||
if (this.selectedBootstrapKeys.length > 0) {
|
||||
return;
|
||||
}
|
||||
const entries = this.dedupeBootstrapEntries(this.pickEligibleCommunityTcpBootstrapForRandom());
|
||||
if (entries.length === 0) {
|
||||
return;
|
||||
}
|
||||
await this.pickRandomTcpBootstraps({ silent: true, auto: true });
|
||||
this.bootstrapAutoPickDone = true;
|
||||
},
|
||||
buildBootstrapPayload(item) {
|
||||
if (item.kind === "discovered") {
|
||||
const iface = item.iface;
|
||||
|
||||
@@ -587,11 +587,20 @@ export default {
|
||||
if (!this.status.has_docs) return [];
|
||||
return this.allLanguages.filter((l) => l.code !== this.currentLang);
|
||||
},
|
||||
reticulumDocsQueryParam() {
|
||||
return this.$route?.query?.reticulum;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
reticulumDocsQueryParam() {
|
||||
this.applyDocumentationRouteQuery();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.fetchStatus();
|
||||
this.fetchMeshChatXDocs();
|
||||
this.statusInterval = setInterval(this.fetchStatus, 2000);
|
||||
this.applyDocumentationRouteQuery();
|
||||
},
|
||||
beforeUnmount() {
|
||||
if (this.statusInterval) {
|
||||
@@ -612,6 +621,9 @@ export default {
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch docs status:", error);
|
||||
}
|
||||
if (this.reticulumDocsQueryParam) {
|
||||
this.applyDocumentationRouteQuery();
|
||||
}
|
||||
},
|
||||
dismissError() {
|
||||
this.status = { ...this.status, last_error: null };
|
||||
@@ -758,6 +770,28 @@ export default {
|
||||
this.searchQuery = "";
|
||||
this.searchResults = [];
|
||||
},
|
||||
applyDocumentationRouteQuery() {
|
||||
const q = this.reticulumDocsQueryParam;
|
||||
if (q === undefined || q === null || q === "") {
|
||||
return;
|
||||
}
|
||||
const raw = Array.isArray(q) ? q[0] : q;
|
||||
if (typeof raw !== "string" || !raw.trim()) {
|
||||
return;
|
||||
}
|
||||
let path = raw.trim();
|
||||
try {
|
||||
path = decodeURIComponent(path);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
path = path.replace(/^\/?(?:reticulum-docs\/)?/, "");
|
||||
if (!path) {
|
||||
return;
|
||||
}
|
||||
this.activeTab = "reticulum";
|
||||
this.selectedReticulumPath = path;
|
||||
},
|
||||
navigateTo(path) {
|
||||
if (path.startsWith("/meshchatx-docs/")) {
|
||||
this.activeTab = "meshchatx";
|
||||
|
||||
@@ -163,6 +163,9 @@
|
||||
<option value="PipeInterface">Pipe Interface (External)</option>
|
||||
<option value="RNodeIPInterface">RNode over IP</option>
|
||||
<option value="BackboneInterface">Backbone (public relay)</option>
|
||||
<option value="__external__">
|
||||
Custom / external module (RNS interfacepath)
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -241,9 +244,9 @@
|
||||
$t("interfaces.discovery_default_bootstrap_only")
|
||||
}}</FormLabel
|
||||
>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
|
||||
{{ $t("interfaces.discovery_default_bootstrap_only_hint") }}
|
||||
</p>
|
||||
<BundledDocsHint
|
||||
paragraph-class="text-xs text-gray-500 dark:text-gray-400 mt-0.5"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-3">
|
||||
@@ -312,13 +315,20 @@
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<FormLabel class="glass-label">Transport identity (hex)</FormLabel>
|
||||
<FormLabel class="glass-label">{{
|
||||
$t("interfaces.backbone_transport_identity_label")
|
||||
}}</FormLabel>
|
||||
<input
|
||||
v-model="newInterfaceTransportIdentity"
|
||||
type="text"
|
||||
placeholder="32 hex chars from the directory"
|
||||
:placeholder="
|
||||
$t('interfaces.backbone_transport_identity_placeholder')
|
||||
"
|
||||
class="input-field font-mono text-xs"
|
||||
/>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
{{ $t("interfaces.backbone_transport_identity_hint") }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-start gap-2">
|
||||
<Toggle
|
||||
@@ -333,9 +343,9 @@
|
||||
$t("interfaces.discovery_default_bootstrap_only")
|
||||
}}</FormLabel
|
||||
>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
|
||||
{{ $t("interfaces.discovery_default_bootstrap_only_hint") }}
|
||||
</p>
|
||||
<BundledDocsHint
|
||||
paragraph-class="text-xs text-gray-500 dark:text-gray-400 mt-0.5"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -524,12 +534,30 @@
|
||||
>
|
||||
<div
|
||||
v-if="newInterfaceType === 'RNodeInterface'"
|
||||
class="flex items-center gap-2 pb-2"
|
||||
class="flex flex-col gap-3 pb-2 sm:flex-row sm:items-center sm:gap-6"
|
||||
>
|
||||
<Toggle id="rnode-use-ip" v-model="newInterfaceRNodeUseIP" />
|
||||
<FormLabel for="rnode-use-ip" class="cursor-pointer mb-0! text-sm"
|
||||
>Connect over network (IP)</FormLabel
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<Toggle
|
||||
id="rnode-use-ip"
|
||||
:model-value="newInterfaceRNodeUseIP"
|
||||
@update:model-value="setRNodeTransportIp"
|
||||
/>
|
||||
<FormLabel for="rnode-use-ip" class="cursor-pointer mb-0! text-sm"
|
||||
>Connect over network (IP)</FormLabel
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Toggle
|
||||
id="rnode-use-ble"
|
||||
:model-value="newInterfaceRNodeUseBle"
|
||||
@update:model-value="setRNodeTransportBle"
|
||||
/>
|
||||
<FormLabel
|
||||
for="rnode-use-ble"
|
||||
class="cursor-pointer mb-0! text-sm"
|
||||
>{{ $t("interfaces.rnode_ble_toggle") }}</FormLabel
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -555,6 +583,28 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="
|
||||
newInterfaceType === 'RNodeInterface' && newInterfaceRNodeUseBle
|
||||
"
|
||||
class="space-y-2"
|
||||
>
|
||||
<FormLabel class="glass-label">{{
|
||||
$t("interfaces.rnode_ble_peer_label")
|
||||
}}</FormLabel>
|
||||
<input
|
||||
v-model="newInterfaceRNodeBlePeer"
|
||||
type="text"
|
||||
:placeholder="$t('interfaces.rnode_ble_peer_placeholder')"
|
||||
class="input-field font-mono text-sm"
|
||||
autocapitalize="off"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
/>
|
||||
<p class="text-[11px] leading-relaxed text-gray-500 dark:text-gray-400">
|
||||
{{ $t("interfaces.rnode_ble_hint") }}
|
||||
</p>
|
||||
</div>
|
||||
<div v-else>
|
||||
<FormLabel class="glass-label">Serial Port</FormLabel>
|
||||
<div class="relative">
|
||||
@@ -1027,6 +1077,59 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- LocalInterface: IPC path used internally by RNS; optional external module -->
|
||||
<div v-if="newInterfaceType === 'LocalInterface'" class="space-y-4">
|
||||
<div class="text-sm text-gray-800 dark:text-zinc-200 leading-snug">
|
||||
{{ $t("interfaces.loopback_local_title") }}
|
||||
</div>
|
||||
<p class="text-xs text-gray-600 dark:text-zinc-400 leading-relaxed">
|
||||
{{ $t("interfaces.loopback_local_body") }}
|
||||
</p>
|
||||
<BundledDocsHint
|
||||
hint-i18n-key="interfaces.loopback_local_docs_hint"
|
||||
link-i18n-key="interfaces.loopback_local_docs_link"
|
||||
:docs-rel-path="docsReticulumInterfacesOverview"
|
||||
paragraph-class="text-xs text-gray-500 dark:text-gray-400"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- External interface module (TypeName.py under Reticulum interfacepath) -->
|
||||
<div v-if="newInterfaceType === '__external__'" class="space-y-4">
|
||||
<p class="text-xs text-gray-600 dark:text-zinc-400 leading-relaxed">
|
||||
{{ $t("interfaces.custom_external_intro") }}
|
||||
</p>
|
||||
<div>
|
||||
<FormLabel class="glass-label">{{
|
||||
$t("interfaces.custom_external_type_label")
|
||||
}}</FormLabel>
|
||||
<input
|
||||
v-model="customExternalTypeName"
|
||||
type="text"
|
||||
class="input-field font-mono text-xs"
|
||||
:placeholder="$t('interfaces.custom_external_type_placeholder')"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<FormLabel class="glass-label">{{
|
||||
$t("interfaces.custom_external_json_label")
|
||||
}}</FormLabel>
|
||||
<textarea
|
||||
v-model="customExternalOptionsJson"
|
||||
rows="10"
|
||||
class="input-field font-mono text-xs min-h-[10rem]"
|
||||
:placeholder="$t('interfaces.custom_external_json_placeholder')"
|
||||
></textarea>
|
||||
</div>
|
||||
<BundledDocsHint
|
||||
hint-i18n-key="interfaces.custom_external_docs_hint"
|
||||
link-i18n-key="interfaces.custom_external_docs_link"
|
||||
:docs-rel-path="docsReticulumInterfacesOverview"
|
||||
paragraph-class="text-xs text-gray-500 dark:text-gray-400"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1232,9 +1335,7 @@
|
||||
<FormLabel class="glass-label mb-0! text-sm">{{
|
||||
$t("interfaces.discovery_default_bootstrap_only")
|
||||
}}</FormLabel>
|
||||
<p class="text-xs text-gray-400">
|
||||
{{ $t("interfaces.discovery_default_bootstrap_only_hint") }}
|
||||
</p>
|
||||
<BundledDocsHint paragraph-class="text-xs text-gray-400" />
|
||||
</div>
|
||||
<Toggle v-model="reticulumDiscovery.default_bootstrap_only" />
|
||||
</div>
|
||||
@@ -1566,6 +1667,8 @@ import FormLabel from "../forms/FormLabel.vue";
|
||||
import Toggle from "../forms/Toggle.vue";
|
||||
import GlobalState from "../../js/GlobalState";
|
||||
import MaterialDesignIcon from "../MaterialDesignIcon.vue";
|
||||
import BundledDocsHint from "./BundledDocsHint.vue";
|
||||
import { RETICULUM_MANUAL_INTERFACES_OVERVIEW_REL } from "../../js/reticulumDocsEntryUrl.js";
|
||||
|
||||
export default {
|
||||
name: "AddInterfacePage",
|
||||
@@ -1574,6 +1677,7 @@ export default {
|
||||
FormLabel,
|
||||
ExpandingSection,
|
||||
Toggle,
|
||||
BundledDocsHint,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -1582,6 +1686,10 @@ export default {
|
||||
isSaving: false,
|
||||
isEditingInterface: false,
|
||||
|
||||
customExternalTypeName: "",
|
||||
customExternalOptionsJson: "{}",
|
||||
docsReticulumInterfacesOverview: RETICULUM_MANUAL_INTERFACES_OVERVIEW_REL,
|
||||
|
||||
config: null,
|
||||
|
||||
communityInterfaces: [],
|
||||
@@ -1673,6 +1781,8 @@ export default {
|
||||
|
||||
newInterfacePort: null,
|
||||
newInterfaceRNodeUseIP: false,
|
||||
newInterfaceRNodeUseBle: false,
|
||||
newInterfaceRNodeBlePeer: "",
|
||||
newInterfaceRNodeIPHost: "localhost",
|
||||
newInterfaceRNodeIPPort: "7633",
|
||||
RNodeGHzValue: 0,
|
||||
@@ -1876,6 +1986,28 @@ export default {
|
||||
console.log(e);
|
||||
}
|
||||
},
|
||||
effectiveRNodeBlePort() {
|
||||
let p = (this.newInterfaceRNodeBlePeer || "").trim();
|
||||
if (!p) {
|
||||
return "ble://";
|
||||
}
|
||||
if (p.toLowerCase().startsWith("ble://")) {
|
||||
return p;
|
||||
}
|
||||
return `ble://${p}`;
|
||||
},
|
||||
setRNodeTransportIp(v) {
|
||||
this.newInterfaceRNodeUseIP = Boolean(v);
|
||||
if (this.newInterfaceRNodeUseIP) {
|
||||
this.newInterfaceRNodeUseBle = false;
|
||||
}
|
||||
},
|
||||
setRNodeTransportBle(v) {
|
||||
this.newInterfaceRNodeUseBle = Boolean(v);
|
||||
if (this.newInterfaceRNodeUseBle) {
|
||||
this.newInterfaceRNodeUseIP = false;
|
||||
}
|
||||
},
|
||||
async loadCommunityInterfaces() {
|
||||
try {
|
||||
const response = await window.api.get(`/api/v1/community-interfaces`);
|
||||
@@ -1916,6 +2048,25 @@ export default {
|
||||
|
||||
this.newInterfaceName = interfaceName;
|
||||
this.newInterfaceType = iface.type;
|
||||
this.customExternalTypeName = "";
|
||||
this.customExternalOptionsJson = "{}";
|
||||
if (!this.isDedicatedFormInterfaceType(iface.type)) {
|
||||
this.newInterfaceType = "__external__";
|
||||
this.customExternalTypeName = iface.type;
|
||||
const skip = new Set(["type", "name", "interface_enabled", "enabled"]);
|
||||
const flat = {};
|
||||
for (const [k, v] of Object.entries(iface)) {
|
||||
if (skip.has(k)) {
|
||||
continue;
|
||||
}
|
||||
if (v !== null && typeof v === "object" && !Array.isArray(v)) {
|
||||
continue;
|
||||
}
|
||||
flat[k] = v;
|
||||
}
|
||||
this.customExternalOptionsJson = JSON.stringify(flat, null, 2);
|
||||
}
|
||||
|
||||
this.newInterfaceTargetHost = iface.target_host ?? iface.remote ?? null;
|
||||
this.newInterfaceTargetPort = iface.target_port ?? null;
|
||||
this.newInterfaceTransportIdentity = iface.transport_identity ?? null;
|
||||
@@ -1982,7 +2133,12 @@ export default {
|
||||
|
||||
this.newInterfacePort = iface.port;
|
||||
this.newInterfaceRNodeUseIP = false;
|
||||
if (iface.port && String(iface.port).startsWith("tcp://")) {
|
||||
this.newInterfaceRNodeUseBle = false;
|
||||
this.newInterfaceRNodeBlePeer = "";
|
||||
if (iface.port && String(iface.port).toLowerCase().startsWith("ble://")) {
|
||||
this.newInterfaceRNodeUseBle = true;
|
||||
this.newInterfaceRNodeBlePeer = String(iface.port);
|
||||
} else if (iface.port && String(iface.port).startsWith("tcp://")) {
|
||||
const addr = String(iface.port).replace("tcp://", "");
|
||||
const parts = addr.split(":");
|
||||
this.newInterfaceRNodeIPHost = parts[0] || "localhost";
|
||||
@@ -2103,7 +2259,12 @@ export default {
|
||||
if (config.forward_port) this.newInterfaceForwardPort = Number(config.forward_port);
|
||||
if (config.port) {
|
||||
this.newInterfacePort = config.port;
|
||||
if (config.port.startsWith("tcp://")) {
|
||||
this.newInterfaceRNodeUseBle = false;
|
||||
this.newInterfaceRNodeUseIP = false;
|
||||
if (String(config.port).toLowerCase().startsWith("ble://")) {
|
||||
this.newInterfaceRNodeUseBle = true;
|
||||
this.newInterfaceRNodeBlePeer = config.port;
|
||||
} else if (config.port.startsWith("tcp://")) {
|
||||
const addr = config.port.replace("tcp://", "");
|
||||
const [host, port] = addr.split(":");
|
||||
this.newInterfaceRNodeIPHost = host;
|
||||
@@ -2391,6 +2552,73 @@ export default {
|
||||
const discoveryEnabled = this.discovery.discoverable === true;
|
||||
const freqHz = Math.round(this.calculateFrequencyInHz());
|
||||
|
||||
if (this.newInterfaceType === "RNodeInterface" && this.newInterfaceRNodeUseBle) {
|
||||
const raw = (this.newInterfaceRNodeBlePeer || "").trim();
|
||||
const inner = raw.toLowerCase().startsWith("ble://") ? raw.slice(6).trim() : raw;
|
||||
if (!inner) {
|
||||
ToastUtils.warning(this.$t("interfaces.rnode_ble_peer_required"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.newInterfaceType === "__external__") {
|
||||
const typeStr = (this.customExternalTypeName || "").trim();
|
||||
if (!typeStr) {
|
||||
ToastUtils.error(this.$t("interfaces.custom_external_type_required"));
|
||||
return;
|
||||
}
|
||||
let extra = {};
|
||||
try {
|
||||
extra = JSON.parse((this.customExternalOptionsJson || "").trim() || "{}");
|
||||
} catch {
|
||||
ToastUtils.error(this.$t("interfaces.custom_external_json_invalid"));
|
||||
return;
|
||||
}
|
||||
if (extra !== null && typeof extra !== "object") {
|
||||
ToastUtils.error(this.$t("interfaces.custom_external_json_invalid"));
|
||||
return;
|
||||
}
|
||||
const payload = {
|
||||
allow_overwriting_interface: this.isEditingInterface,
|
||||
name: this.newInterfaceName,
|
||||
type: typeStr,
|
||||
extra_config: extra,
|
||||
discoverable: discoveryEnabled ? "yes" : null,
|
||||
discovery_name: discoveryEnabled ? this.discovery.discovery_name : null,
|
||||
announce_interval: discoveryEnabled
|
||||
? (this.numOrNull(this.discovery.announce_interval) ?? 360)
|
||||
: null,
|
||||
reachable_on: discoveryEnabled ? this.discovery.reachable_on : null,
|
||||
discovery_stamp_value: discoveryEnabled
|
||||
? (this.numOrNull(this.discovery.discovery_stamp_value) ?? 14)
|
||||
: null,
|
||||
discovery_encrypt: discoveryEnabled ? this.discovery.discovery_encrypt === true : null,
|
||||
publish_ifac: discoveryEnabled ? this.discovery.publish_ifac === true : null,
|
||||
latitude: discoveryEnabled ? this.numOrNull(this.discovery.latitude) : null,
|
||||
longitude: discoveryEnabled ? this.numOrNull(this.discovery.longitude) : null,
|
||||
height: discoveryEnabled ? this.numOrNull(this.discovery.height) : null,
|
||||
discovery_frequency: discoveryEnabled
|
||||
? this.numOrNull(this.discovery.discovery_frequency)
|
||||
: null,
|
||||
discovery_bandwidth: discoveryEnabled
|
||||
? this.numOrNull(this.discovery.discovery_bandwidth)
|
||||
: null,
|
||||
discovery_modulation: discoveryEnabled
|
||||
? this.numOrNull(this.discovery.discovery_modulation)
|
||||
: null,
|
||||
mode: this.sharedInterfaceSettings.mode || null,
|
||||
bitrate: this.sharedInterfaceSettings.bitrate,
|
||||
network_name: this.sharedInterfaceSettings.network_name,
|
||||
passphrase: this.sharedInterfaceSettings.passphrase,
|
||||
};
|
||||
const response = await window.api.post(`/api/v1/reticulum/interfaces/add`, payload);
|
||||
if (response.data.message) ToastUtils.success(response.data.message);
|
||||
GlobalState.hasPendingInterfaceChanges = true;
|
||||
GlobalState.modifiedInterfaceNames.add(this.newInterfaceName);
|
||||
this.$router.push({ name: "interfaces" });
|
||||
return;
|
||||
}
|
||||
|
||||
const i2pPeers =
|
||||
this.newInterfaceType === "I2PInterface"
|
||||
? (this.I2PSettings.newInterfacePeers || []).map((p) => String(p).trim()).filter(Boolean)
|
||||
@@ -2433,7 +2661,9 @@ export default {
|
||||
this.newInterfaceType === "I2PInterface" ? this.newInterfaceConnectable === true : null,
|
||||
port: this.newInterfaceRNodeUseIP
|
||||
? `tcp://${this.newInterfaceRNodeIPHost}:${this.newInterfaceRNodeIPPort}`
|
||||
: this.newInterfacePort,
|
||||
: this.newInterfaceRNodeUseBle
|
||||
? this.effectiveRNodeBlePort()
|
||||
: this.newInterfacePort,
|
||||
frequency: freqHz,
|
||||
bandwidth: this.newInterfaceBandwidth,
|
||||
txpower: this.newInterfaceTxpower,
|
||||
@@ -2555,6 +2785,25 @@ export default {
|
||||
removeSubInterface(idx) {
|
||||
this.RNodeMultiInterface.subInterfaces.splice(idx, 1);
|
||||
},
|
||||
isDedicatedFormInterfaceType(t) {
|
||||
const builtin = new Set([
|
||||
"TCPClientInterface",
|
||||
"BackboneInterface",
|
||||
"I2PInterface",
|
||||
"TCPServerInterface",
|
||||
"UDPInterface",
|
||||
"RNodeInterface",
|
||||
"RNodeIPInterface",
|
||||
"RNodeMultiInterface",
|
||||
"SerialInterface",
|
||||
"KISSInterface",
|
||||
"AX25KISSInterface",
|
||||
"PipeInterface",
|
||||
"AutoInterface",
|
||||
"LocalInterface",
|
||||
]);
|
||||
return builtin.has(t);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
<!-- SPDX-License-Identifier: 0BSD AND MIT -->
|
||||
|
||||
<template>
|
||||
<p :class="paragraphClass">
|
||||
<span>{{ $t(hintI18nKey) }}</span>
|
||||
{{ " " }}
|
||||
<button
|
||||
type="button"
|
||||
class="text-blue-600 dark:text-blue-400 hover:underline font-medium cursor-pointer inline p-0 border-0 bg-transparent text-left align-baseline text-[inherit]"
|
||||
@click="openDocs"
|
||||
>
|
||||
{{ $t(linkI18nKey) }}
|
||||
</button>
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { RETICULUM_MANUAL_INTERFACES_COMMON_OPTIONS_REL } from "../../js/reticulumDocsEntryUrl.js";
|
||||
import { openBundledReticulumManualPath } from "../../js/reticulumDocsNavigation.js";
|
||||
|
||||
export default {
|
||||
name: "BundledDocsHint",
|
||||
props: {
|
||||
paragraphClass: {
|
||||
type: String,
|
||||
default: "text-xs text-gray-500 dark:text-gray-400 mt-0.5",
|
||||
},
|
||||
hintI18nKey: {
|
||||
type: String,
|
||||
default: "interfaces.discovery_default_bootstrap_only_hint",
|
||||
},
|
||||
linkI18nKey: {
|
||||
type: String,
|
||||
default: "interfaces.discovery_default_bootstrap_docs_link",
|
||||
},
|
||||
docsRelPath: {
|
||||
type: String,
|
||||
default: RETICULUM_MANUAL_INTERFACES_COMMON_OPTIONS_REL,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openDocs() {
|
||||
openBundledReticulumManualPath(this.$router, this.docsRelPath);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -656,7 +656,9 @@
|
||||
{{ $t("interfaces.discovery_default_bootstrap_only") }}
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ $t("interfaces.discovery_default_bootstrap_only_hint") }}
|
||||
<BundledDocsHint
|
||||
paragraph-class="text-xs text-gray-500 dark:text-gray-400"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Toggle
|
||||
@@ -723,6 +725,7 @@ import MaterialDesignIcon from "../MaterialDesignIcon.vue";
|
||||
import ToastUtils from "../../js/ToastUtils";
|
||||
import GlobalState from "../../js/GlobalState";
|
||||
import Toggle from "../forms/Toggle.vue";
|
||||
import BundledDocsHint from "./BundledDocsHint.vue";
|
||||
|
||||
export default {
|
||||
name: "InterfacesPage",
|
||||
@@ -731,6 +734,7 @@ export default {
|
||||
ImportInterfacesModal,
|
||||
Interface,
|
||||
MaterialDesignIcon,
|
||||
BundledDocsHint,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user