@@ -1359,7 +1313,7 @@
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--"
+ @click="previousStep"
>
{{ $t("tutorial.back") }}
@@ -1417,12 +1371,17 @@ export default {
return {
visible: false,
currentStep: 1,
- totalSteps: 5,
+ totalSteps: 6,
logoUrl,
communityInterfaces: [],
loadingInterfaces: false,
interfaceAddedViaTutorial: false,
- discoveryOption: null,
+ connectionMode: null,
+ selectedBootstrapKeys: [],
+ addedBootstrapKeys: [],
+ addingBootstraps: false,
+ addingLocal: false,
+ reloadingReticulum: false,
discoveredInterfaces: [],
discoveredActive: [],
loadingDiscovered: false,
@@ -1449,6 +1408,18 @@ export default {
interfacesWithLocation() {
return this.discoveredInterfaces.filter((iface) => iface.latitude != null && iface.longitude != null);
},
+ bootstrapCommunityKey() {
+ return (iface) => `comm:${iface.name}`;
+ },
+ bootstrapDiscoveredKey() {
+ return (iface) => `disc:${iface.discovery_hash || iface.name}`;
+ },
+ hasAnyBootstrapsToShow() {
+ return this.communityInterfaces.length > 0 || this.sortedDiscoveredInterfaces.length > 0;
+ },
+ selectedBootstrapCount() {
+ return this.selectedBootstrapKeys.length;
+ },
},
beforeUnmount() {
if (this.onWindowResize) {
@@ -1498,7 +1469,9 @@ export default {
this.visible = true;
this.currentStep = 1;
this.interfaceAddedViaTutorial = false;
- this.discoveryOption = null;
+ this.connectionMode = null;
+ this.selectedBootstrapKeys = [];
+ this.addedBootstrapKeys = [];
await this.loadCommunityInterfaces();
await this.loadDiscoveredInterfaces();
@@ -1532,17 +1505,34 @@ export default {
this.loadingDiscovered = false;
}
},
- async useDiscovery() {
+ async reloadReticulum() {
+ this.reloadingReticulum = true;
+ try {
+ await window.api.post("/api/v1/reticulum/reload");
+ GlobalState.hasPendingInterfaceChanges = false;
+ if (GlobalState.modifiedInterfaceNames && GlobalState.modifiedInterfaceNames.clear) {
+ GlobalState.modifiedInterfaceNames.clear();
+ }
+ return true;
+ } catch (e) {
+ console.error("Failed to reload Reticulum:", e);
+ ToastUtils.error(this.$t("tutorial.failed_reload_rns"));
+ return false;
+ } finally {
+ this.reloadingReticulum = false;
+ }
+ },
+ async useDiscoveryMode() {
this.savingDiscovery = true;
try {
const payload = {
discover_interfaces: true,
- autoconnect_discovered_interfaces: 3, // default to 3 slots
+ autoconnect_discovered_interfaces: 3,
};
await window.api.patch(`/api/v1/reticulum/discovery`, payload);
ToastUtils.success(this.$t("tutorial.discovery_enabled"));
- this.discoveryOption = "yes";
- this.nextStep();
+ this.connectionMode = "discovery";
+ this.currentStep = 3;
} catch (e) {
console.error("Failed to enable discovery:", e);
ToastUtils.error(this.$t("tutorial.failed_enable_discovery"));
@@ -1550,6 +1540,113 @@ export default {
this.savingDiscovery = false;
}
},
+ async useLocalMode() {
+ if (this.addingLocal) return;
+ this.addingLocal = true;
+ try {
+ await window.api.post("/api/v1/reticulum/interfaces/add", {
+ name: "Local Network",
+ type: "AutoInterface",
+ enabled: true,
+ });
+ this.interfaceAddedViaTutorial = true;
+ GlobalState.hasPendingInterfaceChanges = true;
+ GlobalState.modifiedInterfaceNames.add("Local Network");
+ ToastUtils.success(this.$t("tutorial.local_added"));
+ await this.reloadReticulum();
+ this.connectionMode = "local";
+ this.currentStep = 4;
+ } catch (e) {
+ console.error("Failed to add AutoInterface:", e);
+ ToastUtils.error(e.response?.data?.message || this.$t("tutorial.failed_add_local"));
+ } finally {
+ this.addingLocal = false;
+ }
+ },
+ useManualMode() {
+ this.connectionMode = "manual";
+ this.currentStep = 4;
+ },
+ isBootstrapSelected(key) {
+ return this.selectedBootstrapKeys.includes(key);
+ },
+ toggleBootstrap(key) {
+ const idx = this.selectedBootstrapKeys.indexOf(key);
+ if (idx >= 0) {
+ this.selectedBootstrapKeys.splice(idx, 1);
+ } else {
+ this.selectedBootstrapKeys.push(key);
+ }
+ },
+ buildBootstrapPayload(item) {
+ if (item.kind === "discovered") {
+ const iface = item.iface;
+ const payload = {
+ name: iface.name || `Discovered ${iface.discovery_hash || ""}`.trim(),
+ type: iface.type === "BackboneInterface" ? "TCPClientInterface" : iface.type,
+ enabled: true,
+ };
+ if (iface.reachable_on) {
+ payload.target_host = iface.reachable_on;
+ }
+ if (iface.port) {
+ payload.target_port = iface.port;
+ }
+ return payload;
+ }
+ const iface = item.iface;
+ return {
+ name: iface.name,
+ type: iface.type,
+ target_host: iface.target_host,
+ target_port: iface.target_port,
+ enabled: true,
+ };
+ },
+ async confirmBootstraps() {
+ if (this.addingBootstraps) return;
+ if (this.selectedBootstrapKeys.length === 0) {
+ ToastUtils.warning(this.$t("tutorial.bootstrap_pick_at_least_one"));
+ return;
+ }
+ this.addingBootstraps = true;
+ const items = [];
+ for (const key of this.selectedBootstrapKeys) {
+ if (this.addedBootstrapKeys.includes(key)) continue;
+ if (key.startsWith("comm:")) {
+ const iface = this.communityInterfaces.find((c) => `comm:${c.name}` === key);
+ if (iface) items.push({ key, kind: "community", iface });
+ } else if (key.startsWith("disc:")) {
+ const iface = this.discoveredInterfaces.find((d) => `disc:${d.discovery_hash || d.name}` === key);
+ if (iface) items.push({ key, kind: "discovered", iface });
+ }
+ }
+ let added = 0;
+ for (const item of items) {
+ try {
+ const payload = this.buildBootstrapPayload(item);
+ if (!payload.target_host) continue;
+ await window.api.post("/api/v1/reticulum/interfaces/add", payload);
+ this.addedBootstrapKeys.push(item.key);
+ GlobalState.hasPendingInterfaceChanges = true;
+ GlobalState.modifiedInterfaceNames.add(payload.name);
+ added += 1;
+ } catch (e) {
+ console.error("Failed to add bootstrap interface:", e);
+ ToastUtils.error(e.response?.data?.message || this.$t("tutorial.failed_add_bootstrap"));
+ }
+ }
+ if (added > 0) {
+ this.interfaceAddedViaTutorial = true;
+ ToastUtils.success(this.$t("tutorial.bootstrap_added", { count: added }));
+ await this.reloadReticulum();
+ }
+ this.addingBootstraps = false;
+ this.currentStep = 4;
+ },
+ skipBootstraps() {
+ this.currentStep = 4;
+ },
async enableAutoPropagation() {
this.savingPropagation = true;
try {
@@ -1654,9 +1751,20 @@ export default {
}
},
nextStep() {
- if (this.currentStep < this.totalSteps) {
- this.currentStep++;
+ if (this.currentStep >= this.totalSteps) return;
+ if (this.currentStep === 2 && this.connectionMode !== "discovery") {
+ this.currentStep = 4;
+ return;
}
+ this.currentStep++;
+ },
+ previousStep() {
+ if (this.currentStep <= 1) return;
+ if (this.currentStep === 4 && this.connectionMode !== "discovery") {
+ this.currentStep = 2;
+ return;
+ }
+ this.currentStep--;
},
async skipTutorial() {
if (await DialogUtils.confirm(this.$t("tutorial.skip_confirm"))) {
@@ -1676,10 +1784,13 @@ export default {
}
},
async finishTutorial() {
+ if (GlobalState.hasPendingInterfaceChanges) {
+ await this.reloadReticulum();
+ }
this.visible = false;
this.markSeen();
if (this.interfaceAddedViaTutorial) {
- ToastUtils.info(this.$t("tutorial.ready_desc"));
+ ToastUtils.success(this.$t("tutorial.ready_finished"));
}
},
async onVisibleUpdate(val) {
diff --git a/meshchatx/src/frontend/components/about/AboutPage.vue b/meshchatx/src/frontend/components/about/AboutPage.vue
index b4ee6c7..d70a7c2 100644
--- a/meshchatx/src/frontend/components/about/AboutPage.vue
+++ b/meshchatx/src/frontend/components/about/AboutPage.vue
@@ -38,56 +38,46 @@
-
@@ -1371,6 +1361,12 @@ export default {
if (this.isElectron) {
ElectronUtils.shutdown();
+ } else if (typeof window !== "undefined" && window.MeshChatXAndroid?.exitApp) {
+ try {
+ window.MeshChatXAndroid.exitApp();
+ } catch {
+ ToastUtils.success(this.$t("about.shutdown_sent"));
+ }
} else {
ToastUtils.success(this.$t("about.shutdown_sent"));
}
@@ -1450,4 +1446,7 @@ export default {
outline: 2px solid rgba(59, 130, 246, 0.35);
outline-offset: 2px;
}
+.about-action-btn {
+ @apply min-w-0 min-h-[40px] justify-center whitespace-nowrap;
+}
diff --git a/meshchatx/src/frontend/components/call/CallPage.vue b/meshchatx/src/frontend/components/call/CallPage.vue
index 9b0ef1b..1caa65a 100644
--- a/meshchatx/src/frontend/components/call/CallPage.vue
+++ b/meshchatx/src/frontend/components/call/CallPage.vue
@@ -5,7 +5,7 @@