move nomadnetwork page to own vue component
This commit is contained in:
6
package-lock.json
generated
6
package-lock.json
generated
@@ -10,6 +10,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"electron-prompt": "^1.7.0",
|
"electron-prompt": "^1.7.0",
|
||||||
|
"mitt": "^3.0.1",
|
||||||
"vue-router": "^4.4.2"
|
"vue-router": "^4.4.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -3651,6 +3652,11 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mitt": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="
|
||||||
|
},
|
||||||
"node_modules/mkdirp": {
|
"node_modules/mkdirp": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
|
||||||
|
|||||||
@@ -70,6 +70,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"electron-prompt": "^1.7.0",
|
"electron-prompt": "^1.7.0",
|
||||||
|
"mitt": "^3.0.1",
|
||||||
"vue-router": "^4.4.2"
|
"vue-router": "^4.4.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,14 +54,14 @@
|
|||||||
|
|
||||||
<!-- nomad network -->
|
<!-- nomad network -->
|
||||||
<li>
|
<li>
|
||||||
<button @click="tab = 'nomadnetwork'" type="button" :class="[ tab === 'nomadnetwork' ? 'bg-blue-100 text-blue-800 group:text-blue-800 hover:bg-blue-100' : '']" class="w-full text-gray-800 hover:bg-gray-100 group flex gap-x-3 rounded-r-full p-2 mr-2 text-sm leading-6 font-semibold focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600">
|
<SidebarLink :to="{ name: 'nomadnetwork' }">
|
||||||
<span class="my-auto">
|
<template v-slot:icon>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 21a9.004 9.004 0 0 0 8.716-6.747M12 21a9.004 9.004 0 0 1-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 0 1 7.843 4.582M12 3a8.997 8.997 0 0 0-7.843 4.582m15.686 0A11.953 11.953 0 0 1 12 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0 1 21 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0 1 12 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 0 1 3 12c0-1.605.42-3.113 1.157-4.418" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 21a9.004 9.004 0 0 0 8.716-6.747M12 21a9.004 9.004 0 0 1-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 0 1 7.843 4.582M12 3a8.997 8.997 0 0 0-7.843 4.582m15.686 0A11.953 11.953 0 0 1 12 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0 1 21 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0 1 12 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 0 1 3 12c0-1.605.42-3.113 1.157-4.418" />
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</template>
|
||||||
<span class="my-auto">Nomad Network</span>
|
<template v-slot:text>Nomad Network</template>
|
||||||
</button>
|
</SidebarLink>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<!-- interfaces -->
|
<!-- interfaces -->
|
||||||
@@ -241,135 +241,31 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- messages sidebar -->
|
<RouterView/>
|
||||||
<MessagesSidebar
|
|
||||||
v-if="tab === 'messages'"
|
|
||||||
:conversations="conversations"
|
|
||||||
:peers="peers"
|
|
||||||
:selected-destination-hash="selectedPeer?.destination_hash"
|
|
||||||
@conversation-click="onConversationClick"
|
|
||||||
@peer-click="onPeerClick"/>
|
|
||||||
|
|
||||||
<!-- nomadnetwork sidebar -->
|
<!-- <!– messages sidebar –>-->
|
||||||
<NomadNetworkSidebar
|
<!-- <MessagesSidebar-->
|
||||||
v-if="tab === 'nomadnetwork'"
|
<!-- v-if="tab === 'messages'"-->
|
||||||
:nodes="nodes"
|
<!-- :conversations="conversations"-->
|
||||||
:selected-destination-hash="selectedNode?.destination_hash"
|
<!-- :peers="peers"-->
|
||||||
@node-click="onNodeClick"/>
|
<!-- :selected-destination-hash="selectedPeer?.destination_hash"-->
|
||||||
|
<!-- @conversation-click="onConversationClick"-->
|
||||||
|
<!-- @peer-click="onPeerClick"/>-->
|
||||||
|
|
||||||
<!-- main view -->
|
<!-- <!– main view –>-->
|
||||||
<div class="flex flex-col flex-1 overflow-hidden min-w-full sm:min-w-[500px]">
|
<!-- <div class="flex flex-col flex-1 overflow-hidden min-w-full sm:min-w-[500px]">-->
|
||||||
|
|
||||||
<RouterView/>
|
<!-- <!– messages tab –>-->
|
||||||
|
<!-- <ConversationViewer-->
|
||||||
|
<!-- v-if="tab === 'messages'"-->
|
||||||
|
<!-- ref="conversation-viewer"-->
|
||||||
|
<!-- :my-lxmf-address-hash="config?.lxmf_address_hash"-->
|
||||||
|
<!-- :selected-peer="selectedPeer"-->
|
||||||
|
<!-- :conversations="conversations"-->
|
||||||
|
<!-- @close="selectedPeer = null"-->
|
||||||
|
<!-- @reload-conversations="getConversations"/>-->
|
||||||
|
|
||||||
<!-- messages tab -->
|
<!-- </div>-->
|
||||||
<ConversationViewer
|
|
||||||
v-if="tab === 'messages'"
|
|
||||||
ref="conversation-viewer"
|
|
||||||
:my-lxmf-address-hash="config?.lxmf_address_hash"
|
|
||||||
:selected-peer="selectedPeer"
|
|
||||||
:conversations="conversations"
|
|
||||||
@close="selectedPeer = null"
|
|
||||||
@reload-conversations="getConversations"/>
|
|
||||||
|
|
||||||
<!-- nomadnetwork tab -->
|
|
||||||
<template v-if="tab === 'nomadnetwork'">
|
|
||||||
|
|
||||||
<!-- node -->
|
|
||||||
<div v-if="selectedNode" class="m-2 flex flex-col h-full border rounded-xl bg-white shadow overflow-hidden">
|
|
||||||
|
|
||||||
<!-- header -->
|
|
||||||
<div class="flex p-2 border-b border-gray-300">
|
|
||||||
|
|
||||||
<!-- node info -->
|
|
||||||
<div class="my-auto">
|
|
||||||
<span class="font-semibold">{{ selectedNode.name }}</span>
|
|
||||||
<span v-if="selectedNodePath" @click="onDestinationPathClick(selectedNodePath)" class="text-sm cursor-pointer"> - {{ selectedNodePath.hops }} {{ selectedNodePath.hops === 1 ? 'hop' : 'hops' }} away</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- close button -->
|
|
||||||
<div class="my-auto ml-auto mr-2">
|
|
||||||
<div @click="selectedNode = null" class="cursor-pointer">
|
|
||||||
<div class="flex text-gray-700 bg-gray-100 hover:bg-gray-200 p-1 rounded-full">
|
|
||||||
<div>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
|
|
||||||
<path d="M6.28 5.22a.75.75 0 0 0-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 1 0 1.06 1.06L10 11.06l3.72 3.72a.75.75 0 1 0 1.06-1.06L11.06 10l3.72-3.72a.75.75 0 0 0-1.06-1.06L10 8.94 6.28 5.22Z" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- browser navigation -->
|
|
||||||
<div class="flex w-full border-gray-300 border-b p-2">
|
|
||||||
<button @click="loadNodePage(selectedNode.destination_hash, '/page/index.mu')" type="button" class="my-auto text-gray-500 bg-gray-200 hover:bg-gray-300 rounded p-1 cursor-pointer">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
|
|
||||||
<path fill-rule="evenodd" d="M9.293 2.293a1 1 0 0 1 1.414 0l7 7A1 1 0 0 1 17 11h-1v6a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1v-3a1 1 0 0 0-1-1H9a1 1 0 0 0-1 1v3a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-6H3a1 1 0 0 1-.707-1.707l7-7Z" clip-rule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<button @click="reloadNodePage" type="button" class="ml-1 my-auto text-gray-500 bg-gray-200 hover:bg-gray-300 rounded p-1 cursor-pointer">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
|
|
||||||
<path fill-rule="evenodd" d="M15.312 11.424a5.5 5.5 0 0 1-9.201 2.466l-.312-.311h2.433a.75.75 0 0 0 0-1.5H3.989a.75.75 0 0 0-.75.75v4.242a.75.75 0 0 0 1.5 0v-2.43l.31.31a7 7 0 0 0 11.712-3.138.75.75 0 0 0-1.449-.39Zm1.23-3.723a.75.75 0 0 0 .219-.53V2.929a.75.75 0 0 0-1.5 0V5.36l-.31-.31A7 7 0 0 0 3.239 8.188a.75.75 0 1 0 1.448.389A5.5 5.5 0 0 1 13.89 6.11l.311.31h-2.432a.75.75 0 0 0 0 1.5h4.243a.75.75 0 0 0 .53-.219Z" clip-rule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<button @click="loadPreviousNodePage" type="button" :disabled="nodePagePathHistory.length === 0" :class="[ nodePagePathHistory.length > 0 ? 'text-gray-500 bg-gray-200 hover:bg-gray-300' : 'text-gray-400 bg-gray-100']" class="ml-1 my-auto rounded p-1 cursor-pointer">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
|
|
||||||
<path fill-rule="evenodd" d="M17 10a.75.75 0 0 1-.75.75H5.612l4.158 3.96a.75.75 0 1 1-1.04 1.08l-5.5-5.25a.75.75 0 0 1 0-1.08l5.5-5.25a.75.75 0 1 1 1.04 1.08L5.612 9.25H16.25A.75.75 0 0 1 17 10Z" clip-rule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<div class="my-auto mx-2 w-full">
|
|
||||||
<input v-model="nodePagePathUrlInput" @keyup.enter="onNodePageUrlClick(nodePagePathUrlInput)" type="text" placeholder="Enter Destination URL" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full px-2.5 py-1.5">
|
|
||||||
</div>
|
|
||||||
<button @click="onNodePageUrlClick(nodePagePathUrlInput)" type="button" class="my-auto text-gray-500 bg-gray-200 hover:bg-gray-300 rounded p-1 cursor-pointer">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
|
|
||||||
<path fill-rule="evenodd" d="M3 10a.75.75 0 0 1 .75-.75h10.638L10.23 5.29a.75.75 0 1 1 1.04-1.08l5.5 5.25a.75.75 0 0 1 0 1.08l-5.5 5.25a.75.75 0 1 1-1.04-1.08l4.158-3.96H3.75A.75.75 0 0 1 3 10Z" clip-rule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- page content -->
|
|
||||||
<div class="h-full overflow-y-scroll p-3 bg-black text-white">
|
|
||||||
<div class="flex" v-if="isLoadingNodePage">
|
|
||||||
<div class="my-auto">
|
|
||||||
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
||||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
||||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="my-auto">Loading {{ nodePageProgress }}%</div>
|
|
||||||
</div>
|
|
||||||
<pre v-else v-html="nodePageContent" class="h-full text-wrap"></pre>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- file download bottom bar -->
|
|
||||||
<div v-if="isDownloadingNodeFile" class="flex w-full border-gray-300 border-t p-2">
|
|
||||||
<div class="my-auto mr-2">
|
|
||||||
<svg class="animate-spin h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
||||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
||||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="my-auto">Downloading: {{ nodeFilePath }} ({{ nodeFileProgress }}%)</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- no node selected -->
|
|
||||||
<div v-else class="flex flex-col mx-auto my-auto text-center leading-5">
|
|
||||||
<div class="mx-auto mb-1">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 21a9.004 9.004 0 0 0 8.716-6.747M12 21a9.004 9.004 0 0 1-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 0 1 7.843 4.582M12 3a8.997 8.997 0 0 0-7.843 4.582m15.686 0A11.953 11.953 0 0 1 12 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0 1 21 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0 1 12 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 0 1 3 12c0-1.605.42-3.113 1.157-4.418" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="font-semibold">No Active Node</div>
|
|
||||||
<div>Select a Node to start browsing!</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</template>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -379,24 +275,21 @@
|
|||||||
<script>
|
<script>
|
||||||
import SidebarLink from "./SidebarLink.vue";
|
import SidebarLink from "./SidebarLink.vue";
|
||||||
import MessagesSidebar from "./messages/MessagesSidebar.vue";
|
import MessagesSidebar from "./messages/MessagesSidebar.vue";
|
||||||
import NomadNetworkSidebar from "./nomadnetwork/NomadNetworkSidebar.vue";
|
|
||||||
import ConversationViewer from "./messages/ConversationViewer.vue";
|
import ConversationViewer from "./messages/ConversationViewer.vue";
|
||||||
import DialogUtils from "../js/DialogUtils";
|
import DialogUtils from "../js/DialogUtils";
|
||||||
|
import Utils from "../js/Utils";
|
||||||
|
import WebSocketConnection from "../js/WebSocketConnection";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'App',
|
name: 'App',
|
||||||
components: {
|
components: {
|
||||||
ConversationViewer,
|
ConversationViewer,
|
||||||
NomadNetworkSidebar,
|
|
||||||
MessagesSidebar,
|
MessagesSidebar,
|
||||||
SidebarLink,
|
SidebarLink,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|
||||||
isWebsocketConnected: false,
|
|
||||||
autoReconnectWebsocket: true,
|
|
||||||
|
|
||||||
isShowingMyIdentitySection: true,
|
isShowingMyIdentitySection: true,
|
||||||
isShowingAnnounceSection: true,
|
isShowingAnnounceSection: true,
|
||||||
isShowingCallsSection: true,
|
isShowingCallsSection: true,
|
||||||
@@ -413,36 +306,22 @@ export default {
|
|||||||
peers: {},
|
peers: {},
|
||||||
selectedPeer: null,
|
selectedPeer: null,
|
||||||
|
|
||||||
nodes: {},
|
|
||||||
selectedNode: null,
|
|
||||||
selectedNodePath: null,
|
|
||||||
|
|
||||||
conversations: [],
|
conversations: [],
|
||||||
|
|
||||||
isLoadingNodePage: false,
|
|
||||||
nodePageRequestSequence: 0,
|
|
||||||
nodePagePath: null,
|
|
||||||
nodePagePathUrlInput: null,
|
|
||||||
nodePageContent: null,
|
|
||||||
nodePageProgress: 0,
|
|
||||||
nodePagePathHistory: [],
|
|
||||||
nodePageCache: {},
|
|
||||||
|
|
||||||
isDownloadingNodeFile: false,
|
|
||||||
nodeFilePath: null,
|
|
||||||
nodeFileProgress: 0,
|
|
||||||
|
|
||||||
nomadnetPageDownloadCallbacks: {},
|
|
||||||
nomadnetFileDownloadCallbacks: {},
|
|
||||||
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
created() {
|
||||||
|
// listen for websocket messages
|
||||||
|
WebSocketConnection.on("message", this.onWebsocketMessage);
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
// stop listening for websocket messages
|
||||||
|
WebSocketConnection.off("message", this.onWebsocketMessage);
|
||||||
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
|
||||||
this.getAppInfo();
|
this.getAppInfo();
|
||||||
this.connectWebsocket();
|
|
||||||
this.getLxmfDeliveryAnnounces();
|
this.getLxmfDeliveryAnnounces();
|
||||||
this.getNomadnetworkNodeAnnounces();
|
|
||||||
this.getConversations();
|
this.getConversations();
|
||||||
|
|
||||||
// fixme: this is called by the micron-parser.js
|
// fixme: this is called by the micron-parser.js
|
||||||
@@ -461,181 +340,81 @@ export default {
|
|||||||
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
connectWebsocket: function() {
|
async onWebsocketMessage(message) {
|
||||||
|
const json = JSON.parse(message.data);
|
||||||
// connect to websocket
|
switch(json.type){
|
||||||
this.ws = new WebSocket(location.origin.replace(/^http/, 'ws') + "/ws");
|
case 'config': {
|
||||||
|
this.config = json.config;
|
||||||
this.ws.addEventListener('open', () => {
|
this.displayName = json.config.display_name;
|
||||||
this.isWebsocketConnected = true;
|
break;
|
||||||
});
|
|
||||||
|
|
||||||
this.ws.addEventListener('close', () => {
|
|
||||||
this.isWebsocketConnected = false;
|
|
||||||
if(this.autoReconnectWebsocket){
|
|
||||||
setTimeout(() => {
|
|
||||||
this.connectWebsocket();
|
|
||||||
}, 1000);
|
|
||||||
}
|
}
|
||||||
});
|
case 'announce': {
|
||||||
|
const aspect = json.announce.aspect;
|
||||||
// handle data from reticulum
|
if(aspect === "lxmf.delivery"){
|
||||||
this.ws.onmessage = (message) => {
|
this.updatePeerFromAnnounce(json.announce);
|
||||||
const json = JSON.parse(message.data);
|
|
||||||
switch(json.type){
|
|
||||||
case 'config': {
|
|
||||||
this.config = json.config;
|
|
||||||
this.displayName = json.config.display_name;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'announce': {
|
|
||||||
const aspect = json.announce.aspect;
|
|
||||||
if(aspect === "lxmf.delivery"){
|
|
||||||
this.updatePeerFromAnnounce(json.announce);
|
|
||||||
} else if(aspect === "nomadnetwork.node"){
|
|
||||||
this.updateNodeFromAnnounce(json.announce);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'announced': {
|
|
||||||
// we just announced, update config so we can show the new last updated at
|
|
||||||
this.getConfig();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'incoming_audio_call': {
|
|
||||||
Notification.requestPermission().then((result) => {
|
|
||||||
if(result === "granted"){
|
|
||||||
new window.Notification("Incoming Call", {
|
|
||||||
body: "Someone is calling you.",
|
|
||||||
tag: "new_audio_call", // only ever show one notification at a time
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'lxmf.delivery': {
|
|
||||||
|
|
||||||
// pass lxmf message to conversation viewer
|
|
||||||
const conversationViewer = this.$refs["conversation-viewer"];
|
|
||||||
if(conversationViewer){
|
|
||||||
conversationViewer.onLxmfMessageReceived(json.lxmf_message);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
}
|
|
||||||
case 'lxmf_message_created': {
|
|
||||||
|
|
||||||
// pass lxmf message to conversation viewer
|
|
||||||
const conversationViewer = this.$refs["conversation-viewer"];
|
|
||||||
if(conversationViewer){
|
|
||||||
conversationViewer.onLxmfMessageCreated(json.lxmf_message);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
}
|
|
||||||
case 'lxmf_message_state_updated': {
|
|
||||||
|
|
||||||
// pass lxmf message to conversation viewer
|
|
||||||
const conversationViewer = this.$refs["conversation-viewer"];
|
|
||||||
if(conversationViewer){
|
|
||||||
conversationViewer.onLxmfMessageUpdated(json.lxmf_message);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
}
|
|
||||||
case 'lxmf_message_deleted': {
|
|
||||||
|
|
||||||
// pass lxmf message hash to conversation viewer
|
|
||||||
const conversationViewer = this.$refs["conversation-viewer"];
|
|
||||||
if(conversationViewer){
|
|
||||||
conversationViewer.onLxmfMessageDeleted(json.hash);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
}
|
|
||||||
case 'nomadnet.page.download': {
|
|
||||||
|
|
||||||
// get data from server
|
|
||||||
const nomadnetPageDownload = json.nomadnet_page_download;
|
|
||||||
|
|
||||||
// find download callbacks
|
|
||||||
const getNomadnetPageDownloadCallbackKey = this.getNomadnetPageDownloadCallbackKey(nomadnetPageDownload.destination_hash, nomadnetPageDownload.page_path);
|
|
||||||
const nomadnetPageDownloadCallback = this.nomadnetPageDownloadCallbacks[getNomadnetPageDownloadCallbackKey];
|
|
||||||
if(!nomadnetPageDownloadCallback){
|
|
||||||
console.log("did not find nomadnet page download callback for key: " + getNomadnetPageDownloadCallbackKey);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle success
|
|
||||||
if(nomadnetPageDownload.status === "success" && nomadnetPageDownloadCallback.onSuccessCallback){
|
|
||||||
nomadnetPageDownloadCallback.onSuccessCallback(nomadnetPageDownload.page_content);
|
|
||||||
delete this.nomadnetPageDownloadCallbacks[getNomadnetPageDownloadCallbackKey];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle failure
|
|
||||||
if(nomadnetPageDownload.status === "failure" && nomadnetPageDownloadCallback.onFailureCallback){
|
|
||||||
nomadnetPageDownloadCallback.onFailureCallback(nomadnetPageDownload.failure_reason);
|
|
||||||
delete this.nomadnetPageDownloadCallbacks[getNomadnetPageDownloadCallbackKey];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle progress
|
|
||||||
if(nomadnetPageDownload.status === "progress" && nomadnetPageDownloadCallback.onProgressCallback){
|
|
||||||
nomadnetPageDownloadCallback.onProgressCallback(nomadnetPageDownload.progress);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
}
|
|
||||||
case 'nomadnet.file.download': {
|
|
||||||
|
|
||||||
// get data from server
|
|
||||||
const nomadnetFileDownload = json.nomadnet_file_download;
|
|
||||||
|
|
||||||
// find download callbacks
|
|
||||||
const getNomadnetFileDownloadCallbackKey = this.getNomadnetFileDownloadCallbackKey(nomadnetFileDownload.destination_hash, nomadnetFileDownload.file_path);
|
|
||||||
const nomadnetFileDownloadCallback = this.nomadnetFileDownloadCallbacks[getNomadnetFileDownloadCallbackKey];
|
|
||||||
if(!nomadnetFileDownloadCallback){
|
|
||||||
console.log("did not find nomadnet file download callback for key: " + getNomadnetFileDownloadCallbackKey);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle success
|
|
||||||
if(nomadnetFileDownload.status === "success" && nomadnetFileDownloadCallback.onSuccessCallback){
|
|
||||||
nomadnetFileDownloadCallback.onSuccessCallback(nomadnetFileDownload.file_name, nomadnetFileDownload.file_bytes);
|
|
||||||
delete this.nomadnetFileDownloadCallbacks[getNomadnetFileDownloadCallbackKey];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle failure
|
|
||||||
if(nomadnetFileDownload.status === "failure" && nomadnetFileDownloadCallback.onFailureCallback){
|
|
||||||
nomadnetFileDownloadCallback.onFailureCallback(nomadnetFileDownload.failure_reason);
|
|
||||||
delete this.nomadnetFileDownloadCallbacks[getNomadnetFileDownloadCallbackKey];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle progress
|
|
||||||
if(nomadnetFileDownload.status === "progress" && nomadnetFileDownloadCallback.onProgressCallback){
|
|
||||||
nomadnetFileDownloadCallback.onProgressCallback(nomadnetFileDownload.progress);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
};
|
case 'announced': {
|
||||||
|
// we just announced, update config so we can show the new last updated at
|
||||||
|
this.getConfig();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'incoming_audio_call': {
|
||||||
|
Notification.requestPermission().then((result) => {
|
||||||
|
if(result === "granted"){
|
||||||
|
new window.Notification("Incoming Call", {
|
||||||
|
body: "Someone is calling you.",
|
||||||
|
tag: "new_audio_call", // only ever show one notification at a time
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'lxmf.delivery': {
|
||||||
|
|
||||||
},
|
// pass lxmf message to conversation viewer
|
||||||
disconnectWebsocket: function() {
|
const conversationViewer = this.$refs["conversation-viewer"];
|
||||||
if(this.ws){
|
if(conversationViewer){
|
||||||
this.ws.close();
|
conversationViewer.onLxmfMessageReceived(json.lxmf_message);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
}
|
||||||
|
case 'lxmf_message_created': {
|
||||||
|
|
||||||
|
// pass lxmf message to conversation viewer
|
||||||
|
const conversationViewer = this.$refs["conversation-viewer"];
|
||||||
|
if(conversationViewer){
|
||||||
|
conversationViewer.onLxmfMessageCreated(json.lxmf_message);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
}
|
||||||
|
case 'lxmf_message_state_updated': {
|
||||||
|
|
||||||
|
// pass lxmf message to conversation viewer
|
||||||
|
const conversationViewer = this.$refs["conversation-viewer"];
|
||||||
|
if(conversationViewer){
|
||||||
|
conversationViewer.onLxmfMessageUpdated(json.lxmf_message);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
}
|
||||||
|
case 'lxmf_message_deleted': {
|
||||||
|
|
||||||
|
// pass lxmf message hash to conversation viewer
|
||||||
|
const conversationViewer = this.$refs["conversation-viewer"];
|
||||||
|
if(conversationViewer){
|
||||||
|
conversationViewer.onLxmfMessageDeleted(json.hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async getAppInfo() {
|
async getAppInfo() {
|
||||||
@@ -670,21 +449,14 @@ export default {
|
|||||||
|
|
||||||
},
|
},
|
||||||
async updateConfig(config) {
|
async updateConfig(config) {
|
||||||
|
|
||||||
// do nothing if not connected to websocket
|
|
||||||
if(!this.isWebsocketConnected){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.ws.send(JSON.stringify({
|
WebSocketConnection.send(JSON.stringify({
|
||||||
"type": "config.set",
|
"type": "config.set",
|
||||||
"config": config,
|
"config": config,
|
||||||
}));
|
}));
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
async saveIdentitySettings() {
|
async saveIdentitySettings() {
|
||||||
await this.updateConfig({
|
await this.updateConfig({
|
||||||
@@ -728,68 +500,6 @@ export default {
|
|||||||
destination_hash: destinationHash,
|
destination_hash: destinationHash,
|
||||||
});
|
});
|
||||||
|
|
||||||
},
|
|
||||||
downloadNomadNetFile(destinationHash, filePath, onSuccessCallback, onFailureCallback, onProgressCallback) {
|
|
||||||
|
|
||||||
// do nothing if not connected to websocket
|
|
||||||
if(!this.isWebsocketConnected){
|
|
||||||
DialogUtils.alert("Not connected to WebSocket!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
|
|
||||||
// set callbacks for nomadnet filePath download
|
|
||||||
this.nomadnetFileDownloadCallbacks[this.getNomadnetFileDownloadCallbackKey(destinationHash, filePath)] = {
|
|
||||||
onSuccessCallback: onSuccessCallback,
|
|
||||||
onFailureCallback: onFailureCallback,
|
|
||||||
onProgressCallback: onProgressCallback,
|
|
||||||
};
|
|
||||||
|
|
||||||
// ask reticulum to download file from nomadnet
|
|
||||||
this.ws.send(JSON.stringify({
|
|
||||||
"type": "nomadnet.file.download",
|
|
||||||
"nomadnet_file_download": {
|
|
||||||
"destination_hash": destinationHash,
|
|
||||||
"file_path": filePath,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
} catch(e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
downloadNomadNetPage(destinationHash, pagePath, onSuccessCallback, onFailureCallback, onProgressCallback) {
|
|
||||||
|
|
||||||
// do nothing if not connected to websocket
|
|
||||||
if(!this.isWebsocketConnected){
|
|
||||||
DialogUtils.alert("Not connected to WebSocket!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
|
|
||||||
// set callbacks for nomadnet page download
|
|
||||||
this.nomadnetPageDownloadCallbacks[this.getNomadnetPageDownloadCallbackKey(destinationHash, pagePath)] = {
|
|
||||||
onSuccessCallback: onSuccessCallback,
|
|
||||||
onFailureCallback: onFailureCallback,
|
|
||||||
onProgressCallback: onProgressCallback,
|
|
||||||
};
|
|
||||||
|
|
||||||
// ask reticulum to download page from nomadnet
|
|
||||||
this.ws.send(JSON.stringify({
|
|
||||||
"type": "nomadnet.page.download",
|
|
||||||
"nomadnet_page_download": {
|
|
||||||
"destination_hash": destinationHash,
|
|
||||||
"page_path": pagePath,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
} catch(e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
},
|
||||||
async getLxmfDeliveryAnnounces() {
|
async getLxmfDeliveryAnnounces() {
|
||||||
try {
|
try {
|
||||||
@@ -812,27 +522,6 @@ export default {
|
|||||||
console.log(e);
|
console.log(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async getNomadnetworkNodeAnnounces() {
|
|
||||||
try {
|
|
||||||
|
|
||||||
// fetch announces for "nomadnetwork.node" aspect
|
|
||||||
const response = await window.axios.get(`/api/v1/announces`, {
|
|
||||||
params: {
|
|
||||||
aspect: "nomadnetwork.node",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// update ui
|
|
||||||
const nodeAnnounces = response.data.announces;
|
|
||||||
for(const nodeAnnounce of nodeAnnounces){
|
|
||||||
this.updateNodeFromAnnounce(nodeAnnounce);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch(e) {
|
|
||||||
// do nothing if failed to load announces
|
|
||||||
console.log(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async getConversations() {
|
async getConversations() {
|
||||||
try {
|
try {
|
||||||
const response = await window.axios.get(`/api/v1/lxmf/conversations`);
|
const response = await window.axios.get(`/api/v1/lxmf/conversations`);
|
||||||
@@ -842,46 +531,14 @@ export default {
|
|||||||
console.log(e);
|
console.log(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async getNodePath(destinationHash) {
|
|
||||||
|
|
||||||
// clear previous known path
|
|
||||||
this.selectedNodePath = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
|
|
||||||
// get path to destination
|
|
||||||
const response = await window.axios.get(`/api/v1/destination/${destinationHash}/path`);
|
|
||||||
|
|
||||||
// update ui
|
|
||||||
this.selectedNodePath = response.data.path;
|
|
||||||
|
|
||||||
} catch(e) {
|
|
||||||
console.log(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
decodeBase64ToUtf8String: function(base64) {
|
|
||||||
// support for decoding base64 as a utf8 string to support emojis and cyrillic characters etc
|
|
||||||
return decodeURIComponent(atob(base64).split('').map(function(c) {
|
|
||||||
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
|
|
||||||
}).join(''));
|
|
||||||
},
|
|
||||||
getPeerNameFromAppData: function(appData) {
|
getPeerNameFromAppData: function(appData) {
|
||||||
try {
|
try {
|
||||||
// app data should be peer name, and our server provides it base64 encoded
|
// app data should be peer name, and our server provides it base64 encoded
|
||||||
return this.decodeBase64ToUtf8String(appData);
|
return Utils.decodeBase64ToUtf8String(appData);
|
||||||
} catch(e){
|
} catch(e){
|
||||||
return "Anonymous Peer";
|
return "Anonymous Peer";
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getNodeNameFromAppData: function(appData) {
|
|
||||||
try {
|
|
||||||
// app data should be node name, and our server provides it base64 encoded
|
|
||||||
return this.decodeBase64ToUtf8String(appData);
|
|
||||||
} catch(e){
|
|
||||||
return "Anonymous Node";
|
|
||||||
}
|
|
||||||
},
|
|
||||||
updatePeerFromAnnounce: function(announce) {
|
updatePeerFromAnnounce: function(announce) {
|
||||||
this.peers[announce.destination_hash] = {
|
this.peers[announce.destination_hash] = {
|
||||||
...announce,
|
...announce,
|
||||||
@@ -889,243 +546,6 @@ export default {
|
|||||||
name: this.getPeerNameFromAppData(announce.app_data),
|
name: this.getPeerNameFromAppData(announce.app_data),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
updateNodeFromAnnounce: function(announce) {
|
|
||||||
this.nodes[announce.destination_hash] = {
|
|
||||||
...announce,
|
|
||||||
// helper property for easily grabbing node name from app data
|
|
||||||
name: this.getNodeNameFromAppData(announce.app_data),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
async loadNodePage(destinationHash, pagePath, addToHistory = true, loadFromCache = true) {
|
|
||||||
|
|
||||||
// get new sequence for this page load
|
|
||||||
const seq = ++this.nodePageRequestSequence;
|
|
||||||
|
|
||||||
// get previous page path
|
|
||||||
const previousNodePagePath = this.nodePagePath;
|
|
||||||
|
|
||||||
// update ui
|
|
||||||
this.isLoadingNodePage = true;
|
|
||||||
this.nodePagePath = `${destinationHash}:${pagePath}`;
|
|
||||||
this.nodePageContent = null;
|
|
||||||
this.nodePageProgress = 0;
|
|
||||||
|
|
||||||
// update url bar
|
|
||||||
this.nodePagePathUrlInput = this.nodePagePath;
|
|
||||||
|
|
||||||
// update node path
|
|
||||||
this.getNodePath(destinationHash);
|
|
||||||
|
|
||||||
// add to previous page to history if we are not loading that previous page
|
|
||||||
if(addToHistory && previousNodePagePath != null && previousNodePagePath !== this.nodePagePath){
|
|
||||||
this.nodePagePathHistory.push(previousNodePagePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if we can load this page from the cache
|
|
||||||
if(loadFromCache){
|
|
||||||
|
|
||||||
// load from cache
|
|
||||||
const nodePagePathCacheKey = `${destinationHash}:${pagePath}`;
|
|
||||||
const cachedNodePageContent = this.nodePageCache[nodePagePathCacheKey];
|
|
||||||
|
|
||||||
// if page is cache, we can just return it now
|
|
||||||
if(cachedNodePageContent != null){
|
|
||||||
this.nodePageContent = cachedNodePageContent;
|
|
||||||
this.isLoadingNodePage = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
this.downloadNomadNetPage(destinationHash, pagePath, (pageContent) => {
|
|
||||||
|
|
||||||
// do nothing if callback is for a previous request
|
|
||||||
if(seq !== this.nodePageRequestSequence){
|
|
||||||
console.log("ignoring page content callback for previous page request")
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// convert micron to html if page ends with .mu extension
|
|
||||||
// otherwise, we will just serve the content as is
|
|
||||||
if(pagePath.endsWith(".mu")){
|
|
||||||
this.nodePageContent = MicronParser.convertMicronToHtml(pageContent);
|
|
||||||
} else {
|
|
||||||
this.nodePageContent = pageContent;
|
|
||||||
}
|
|
||||||
|
|
||||||
// update cache
|
|
||||||
const nodePagePathCacheKey = `${destinationHash}:${pagePath}`;
|
|
||||||
this.nodePageCache[nodePagePathCacheKey] = this.nodePageContent;
|
|
||||||
|
|
||||||
// update page content
|
|
||||||
this.isLoadingNodePage = false;
|
|
||||||
|
|
||||||
// update node path
|
|
||||||
this.getNodePath(destinationHash);
|
|
||||||
|
|
||||||
}, (failureReason) => {
|
|
||||||
|
|
||||||
// do nothing if callback is for a previous request
|
|
||||||
if(seq !== this.nodePageRequestSequence){
|
|
||||||
console.log("ignoring failure callback for previous page request")
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// update page content
|
|
||||||
this.nodePageContent = `Failed loading page: ${failureReason}`;
|
|
||||||
this.isLoadingNodePage = false;
|
|
||||||
|
|
||||||
// update node path
|
|
||||||
this.getNodePath(destinationHash);
|
|
||||||
|
|
||||||
}, (progress) => {
|
|
||||||
|
|
||||||
// do nothing if callback is for a previous request
|
|
||||||
if(seq !== this.nodePageRequestSequence){
|
|
||||||
console.log("ignoring progress callback for previous page request")
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// update page content
|
|
||||||
this.nodePageProgress = Math.round(progress * 100);
|
|
||||||
|
|
||||||
});
|
|
||||||
},
|
|
||||||
async reloadNodePage() {
|
|
||||||
|
|
||||||
// reload current node page without adding to history and without using cache
|
|
||||||
this.onNodePageUrlClick(this.nodePagePath, false, false);
|
|
||||||
|
|
||||||
},
|
|
||||||
async loadPreviousNodePage() {
|
|
||||||
|
|
||||||
// get the previous path from history, or do nothing
|
|
||||||
const previousNodePagePath = this.nodePagePathHistory.pop();
|
|
||||||
if(!previousNodePagePath){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// load the page
|
|
||||||
this.onNodePageUrlClick(previousNodePagePath, false);
|
|
||||||
|
|
||||||
},
|
|
||||||
parseNomadnetworkUrl: function(url) {
|
|
||||||
|
|
||||||
// parse relative urls
|
|
||||||
if(url.startsWith(":")){
|
|
||||||
return {
|
|
||||||
destination_hash: null, // node hash was not in provided url
|
|
||||||
path: url.substring(1), // remove leading ":"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// parse absolute urls such as 00000000000000000000000000000000:/page/index.mu
|
|
||||||
if(url.includes(":")){
|
|
||||||
|
|
||||||
// parse destination hash and url
|
|
||||||
const [destinationHash, relativeUrl] = url.split(":");
|
|
||||||
|
|
||||||
// ensure destination is expected length
|
|
||||||
if(destinationHash.length === 32){
|
|
||||||
return {
|
|
||||||
destination_hash: destinationHash,
|
|
||||||
path: relativeUrl,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// parse node id only
|
|
||||||
if(url.length === 32){
|
|
||||||
return {
|
|
||||||
destination_hash: url,
|
|
||||||
path: "/page/index.mu",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// unsupported url
|
|
||||||
return null;
|
|
||||||
|
|
||||||
},
|
|
||||||
onNodePageUrlClick: function(url, addToHistory = true, useCache = true) {
|
|
||||||
|
|
||||||
// open http urls in new tab
|
|
||||||
if(url.startsWith("http://") || url.startsWith("https://")){
|
|
||||||
window.open(url, "_blank");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// lxmf urls should open the conversation
|
|
||||||
if(url.startsWith("lxmf@")){
|
|
||||||
const destinationHash = url.replace("lxmf@", "");
|
|
||||||
if(destinationHash.length === 32){
|
|
||||||
this.openLXMFConversation(destinationHash);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// attempt to parse url
|
|
||||||
const parsedUrl = this.parseNomadnetworkUrl(url);
|
|
||||||
if(parsedUrl != null){
|
|
||||||
|
|
||||||
// use parsed destination hash, or fallback to selected node destination hash
|
|
||||||
const destinationHash = parsedUrl.destination_hash || this.selectedNode.destination_hash;
|
|
||||||
|
|
||||||
// download file
|
|
||||||
if(parsedUrl.path.startsWith("/file/")){
|
|
||||||
|
|
||||||
// prevent simultaneous downloads
|
|
||||||
if(this.isDownloadingNodeFile){
|
|
||||||
DialogUtils.alert("An existing download is in progress. Please wait for it to finish beforing starting another download.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// update ui
|
|
||||||
this.isDownloadingNodeFile = true;
|
|
||||||
this.nodeFilePath = parsedUrl.path.split("/").pop();
|
|
||||||
this.nodeFileProgress = 0;
|
|
||||||
|
|
||||||
// start file download
|
|
||||||
this.downloadNomadNetFile(destinationHash, parsedUrl.path, (fileName, fileBytesBase64) => {
|
|
||||||
|
|
||||||
// no longer downloading
|
|
||||||
this.isDownloadingNodeFile = false;
|
|
||||||
|
|
||||||
// download file to browser
|
|
||||||
this.downloadFileFromBase64(fileName, fileBytesBase64);
|
|
||||||
|
|
||||||
}, (failureReason) => {
|
|
||||||
|
|
||||||
// no longer downloading
|
|
||||||
this.isDownloadingNodeFile = false;
|
|
||||||
|
|
||||||
// show error message
|
|
||||||
DialogUtils.alert(`Failed to download file: ${failureReason}`);
|
|
||||||
|
|
||||||
}, (progress) => {
|
|
||||||
this.nodeFileProgress = Math.round(progress * 100);
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// update selected node, so relative urls work correctly when returned by the new node
|
|
||||||
this.selectedNode = this.nodes[destinationHash] || {
|
|
||||||
name: "Unknown Node",
|
|
||||||
destination_hash: destinationHash,
|
|
||||||
};
|
|
||||||
|
|
||||||
// navigate to node page
|
|
||||||
this.loadNodePage(destinationHash, parsedUrl.path, addToHistory, useCache);
|
|
||||||
return;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// unsupported url
|
|
||||||
DialogUtils.alert("unsupported url: " + url);
|
|
||||||
|
|
||||||
},
|
|
||||||
downloadFileFromBase64: async function(fileName, fileBytesBase64) {
|
downloadFileFromBase64: async function(fileName, fileBytesBase64) {
|
||||||
|
|
||||||
// create blob from base64 encoded file bytes
|
// create blob from base64 encoded file bytes
|
||||||
@@ -1161,11 +581,6 @@ export default {
|
|||||||
this.selectedPeer = peer;
|
this.selectedPeer = peer;
|
||||||
this.tab = "messages";
|
this.tab = "messages";
|
||||||
},
|
},
|
||||||
onNodeClick: function(node) {
|
|
||||||
this.selectedNode = node;
|
|
||||||
this.tab = "nomadnetwork";
|
|
||||||
this.loadNodePage(node.destination_hash, "/page/index.mu");
|
|
||||||
},
|
|
||||||
onConversationClick: function(conversation) {
|
onConversationClick: function(conversation) {
|
||||||
|
|
||||||
// object must stay compatible with format of peers
|
// object must stay compatible with format of peers
|
||||||
@@ -1232,12 +647,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
getNomadnetPageDownloadCallbackKey: function(destinationHash, pagePath) {
|
|
||||||
return `${destinationHash}:${pagePath}`;
|
|
||||||
},
|
|
||||||
getNomadnetFileDownloadCallbackKey: function(destinationHash, filePath) {
|
|
||||||
return `${destinationHash}:${filePath}`;
|
|
||||||
},
|
|
||||||
formatBytes: function(bytes) {
|
formatBytes: function(bytes) {
|
||||||
|
|
||||||
if(bytes === 0){
|
if(bytes === 0){
|
||||||
|
|||||||
@@ -1,69 +1,71 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="overflow-y-auto space-y-2 p-2">
|
<div class="flex flex-col flex-1 overflow-hidden min-w-full sm:min-w-[500px]">
|
||||||
|
<div class="overflow-y-auto space-y-2 p-2">
|
||||||
|
|
||||||
<!-- app info -->
|
<!-- app info -->
|
||||||
<div v-if="appInfo" class="bg-white rounded shadow">
|
<div v-if="appInfo" class="bg-white rounded shadow">
|
||||||
<div class="flex border-b border-gray-300 text-gray-700 p-2 font-semibold">App Info</div>
|
<div class="flex border-b border-gray-300 text-gray-700 p-2 font-semibold">App Info</div>
|
||||||
<div class="divide-y text-gray-900">
|
<div class="divide-y text-gray-900">
|
||||||
<div class="flex p-1">
|
<div class="flex p-1">
|
||||||
<div class="mr-auto">
|
<div class="mr-auto">
|
||||||
<div>Version</div>
|
<div>Version</div>
|
||||||
<div class="text-sm text-gray-700">v{{ appInfo.version }}</div>
|
<div class="text-sm text-gray-700">v{{ appInfo.version }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="mx-2 my-auto">
|
||||||
|
<a target="_blank" href="https://github.com/liamcottle/reticulum-meshchat/releases" type="button" class="my-auto inline-flex items-center gap-x-1 rounded-md bg-gray-500 px-2 py-1 text-sm font-semibold text-white shadow-sm hover:bg-gray-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-500">
|
||||||
|
Check for Updates
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mx-2 my-auto">
|
<div class="flex p-1">
|
||||||
<a target="_blank" href="https://github.com/liamcottle/reticulum-meshchat/releases" type="button" class="my-auto inline-flex items-center gap-x-1 rounded-md bg-gray-500 px-2 py-1 text-sm font-semibold text-white shadow-sm hover:bg-gray-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-500">
|
<div class="mr-auto">
|
||||||
Check for Updates
|
<div>Reticulum Config Path</div>
|
||||||
</a>
|
<div class="text-sm text-gray-700">{{ appInfo.reticulum_config_path }}</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="isElectron" class="mx-2 my-auto">
|
||||||
|
<button @click="showReticulumConfigFile" type="button" class="my-auto inline-flex items-center gap-x-1 rounded-md bg-gray-500 px-2 py-1 text-sm font-semibold text-white shadow-sm hover:bg-gray-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-500">
|
||||||
|
Show in Folder
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="flex p-1">
|
||||||
<div class="flex p-1">
|
<div class="mr-auto">
|
||||||
<div class="mr-auto">
|
<div>Database Path</div>
|
||||||
<div>Reticulum Config Path</div>
|
<div class="text-sm text-gray-700">{{ appInfo.database_path }}</div>
|
||||||
<div class="text-sm text-gray-700">{{ appInfo.reticulum_config_path }}</div>
|
</div>
|
||||||
|
<div v-if="isElectron" class="mx-2 my-auto">
|
||||||
|
<button @click="showDatabaseFile" type="button" class="my-auto inline-flex items-center gap-x-1 rounded-md bg-gray-500 px-2 py-1 text-sm font-semibold text-white shadow-sm hover:bg-gray-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-500">
|
||||||
|
Show in Folder
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="isElectron" class="mx-2 my-auto">
|
<div class="p-1">
|
||||||
<button @click="showReticulumConfigFile" type="button" class="my-auto inline-flex items-center gap-x-1 rounded-md bg-gray-500 px-2 py-1 text-sm font-semibold text-white shadow-sm hover:bg-gray-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-500">
|
<div>Database File Size</div>
|
||||||
Show in Folder
|
<div class="text-sm text-gray-700">{{ formatBytes(appInfo.database_file_size) }}</div>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex p-1">
|
|
||||||
<div class="mr-auto">
|
|
||||||
<div>Database Path</div>
|
|
||||||
<div class="text-sm text-gray-700">{{ appInfo.database_path }}</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="isElectron" class="mx-2 my-auto">
|
|
||||||
<button @click="showDatabaseFile" type="button" class="my-auto inline-flex items-center gap-x-1 rounded-md bg-gray-500 px-2 py-1 text-sm font-semibold text-white shadow-sm hover:bg-gray-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-500">
|
|
||||||
Show in Folder
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="p-1">
|
|
||||||
<div>Database File Size</div>
|
|
||||||
<div class="text-sm text-gray-700">{{ formatBytes(appInfo.database_file_size) }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- my addresses -->
|
<!-- my addresses -->
|
||||||
<div v-if="config" class="bg-white rounded shadow">
|
<div v-if="config" class="bg-white rounded shadow">
|
||||||
<div class="flex border-b border-gray-300 text-gray-700 p-2 font-semibold">My Addresses</div>
|
<div class="flex border-b border-gray-300 text-gray-700 p-2 font-semibold">My Addresses</div>
|
||||||
<div class="divide-y text-gray-900">
|
<div class="divide-y text-gray-900">
|
||||||
<div class="p-1">
|
<div class="p-1">
|
||||||
<div>Identity Hash</div>
|
<div>Identity Hash</div>
|
||||||
<div class="text-sm text-gray-700">{{ config.identity_hash }}</div>
|
<div class="text-sm text-gray-700">{{ config.identity_hash }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="p-1">
|
<div class="p-1">
|
||||||
<div>LXMF Address</div>
|
<div>LXMF Address</div>
|
||||||
<div class="text-sm text-gray-700">{{ config.lxmf_address_hash }}</div>
|
<div class="text-sm text-gray-700">{{ config.lxmf_address_hash }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="p-1">
|
<div class="p-1">
|
||||||
<div>Audio Call Address</div>
|
<div>Audio Call Address</div>
|
||||||
<div class="text-sm text-gray-700">{{ config.audio_call_address_hash }}</div>
|
<div class="text-sm text-gray-700">{{ config.audio_call_address_hash }}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,343 +1,346 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<div class="flex flex-col flex-1 overflow-hidden min-w-full sm:min-w-[500px]">
|
||||||
|
|
||||||
<div v-if="tab === 'interfaces'" class="overflow-y-auto p-2 space-y-2">
|
<!-- interfaces tab -->
|
||||||
|
<div v-if="tab === 'interfaces'" class="overflow-y-auto p-2 space-y-2">
|
||||||
|
|
||||||
<!-- warning -->
|
<!-- warning -->
|
||||||
<div class="flex bg-orange-500 p-2 text-sm font-semibold leading-6 text-white rounded shadow">
|
<div class="flex bg-orange-500 p-2 text-sm font-semibold leading-6 text-white rounded shadow">
|
||||||
<div class="my-auto">
|
<div class="my-auto">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z" />
|
||||||
</svg>
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="ml-2 my-auto">Reticulum MeshChat must be restarted for any interface changes to take effect.</div>
|
||||||
|
<button v-if="isElectron" @click="relaunch" type="button" class="ml-auto my-auto inline-flex items-center gap-x-1 rounded-md bg-white px-2 py-1 text-sm font-semibold text-black shadow-sm hover:bg-gray-50 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white">
|
||||||
|
<span>Restart Now</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-2 my-auto">Reticulum MeshChat must be restarted for any interface changes to take effect.</div>
|
|
||||||
<button v-if="isElectron" @click="relaunch" type="button" class="ml-auto my-auto inline-flex items-center gap-x-1 rounded-md bg-white px-2 py-1 text-sm font-semibold text-black shadow-sm hover:bg-gray-50 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white">
|
<button @click="showAddInterfaceForm" type="button" class="my-auto inline-flex items-center gap-x-1 rounded-md bg-gray-500 px-2 py-1 text-sm font-semibold text-white shadow-sm hover:bg-gray-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-500">
|
||||||
<span>Restart Now</span>
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-5">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||||
|
</svg>
|
||||||
|
<span>Add Interface</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
|
|
||||||
<button @click="showAddInterfaceForm" type="button" class="my-auto inline-flex items-center gap-x-1 rounded-md bg-gray-500 px-2 py-1 text-sm font-semibold text-white shadow-sm hover:bg-gray-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-500">
|
<!-- interface list -->
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-5">
|
<div v-for="iface of interfacesWithStats" class="border rounded bg-white shadow overflow-hidden">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
|
||||||
</svg>
|
|
||||||
<span>Add Interface</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- interface list -->
|
<!-- IFAC info -->
|
||||||
<div v-for="iface of interfacesWithStats" class="border rounded bg-white shadow overflow-hidden">
|
<div v-if="iface._stats?.ifac_signature != null" class="bg-gray-50 p-1 text-sm text-gray-500 space-x-1 border-b">
|
||||||
|
<div class="flex text-sm">
|
||||||
<!-- IFAC info -->
|
<div class="my-auto">
|
||||||
<div v-if="iface._stats?.ifac_signature != null" class="bg-gray-50 p-1 text-sm text-gray-500 space-x-1 border-b">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-4 text-green-500">
|
||||||
<div class="flex text-sm">
|
<path fill-rule="evenodd" d="M10 1a4.5 4.5 0 0 0-4.5 4.5V9H5a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-6a2 2 0 0 0-2-2h-.5V5.5A4.5 4.5 0 0 0 10 1Zm3 8V5.5a3 3 0 1 0-6 0V9h6Z" clip-rule="evenodd" />
|
||||||
<div class="my-auto">
|
</svg>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-4 text-green-500">
|
</div>
|
||||||
<path fill-rule="evenodd" d="M10 1a4.5 4.5 0 0 0-4.5 4.5V9H5a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-6a2 2 0 0 0-2-2h-.5V5.5A4.5 4.5 0 0 0 10 1Zm3 8V5.5a3 3 0 1 0-6 0V9h6Z" clip-rule="evenodd" />
|
<span class="ml-1 my-auto">
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<span class="ml-1 my-auto">
|
|
||||||
<span class="text-green-500">{{ iface._stats.ifac_size * 8 }}-bit IFAC</span> with sig <span @click="onIFACSignatureClick(iface._stats.ifac_signature)" class="cursor-pointer"><{{ iface._stats.ifac_signature.slice(0, 6) }}...{{ iface._stats.ifac_signature.slice(-6) }}></span>
|
<span class="text-green-500">{{ iface._stats.ifac_size * 8 }}-bit IFAC</span> with sig <span @click="onIFACSignatureClick(iface._stats.ifac_signature)" class="cursor-pointer"><{{ iface._stats.ifac_signature.slice(0, 6) }}...{{ iface._stats.ifac_signature.slice(-6) }}></span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex py-2">
|
|
||||||
|
|
||||||
<!-- icon -->
|
|
||||||
<div class="my-auto mx-2">
|
|
||||||
|
|
||||||
<svg v-if="iface.type === 'AutoInterface'" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 256 256" class="size-6">
|
|
||||||
<path d="M219.31,108.68l-80-80a16,16,0,0,0-22.62,0l-80,80A15.87,15.87,0,0,0,32,120v96a8,8,0,0,0,8,8h64a8,8,0,0,0,8-8V160h32v56a8,8,0,0,0,8,8h64a8,8,0,0,0,8-8V120A15.87,15.87,0,0,0,219.31,108.68ZM208,208H160V152a8,8,0,0,0-8-8H104a8,8,0,0,0-8,8v56H48V120l80-80,80,80Z"></path>
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
<svg v-else-if="iface.type === 'RNodeInterface'" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 256 256" class="size-6">
|
|
||||||
<path d="M128,88a40,40,0,1,0,40,40A40,40,0,0,0,128,88Zm0,64a24,24,0,1,1,24-24A24,24,0,0,1,128,152Zm73.71,7.14a80,80,0,0,1-14.08,22.2,8,8,0,0,1-11.92-10.67,63.95,63.95,0,0,0,0-85.33,8,8,0,1,1,11.92-10.67,80.08,80.08,0,0,1,14.08,84.47ZM69,103.09a64,64,0,0,0,11.26,67.58,8,8,0,0,1-11.92,10.67,79.93,79.93,0,0,1,0-106.67A8,8,0,1,1,80.29,85.34,63.77,63.77,0,0,0,69,103.09ZM248,128a119.58,119.58,0,0,1-34.29,84,8,8,0,1,1-11.42-11.2,103.9,103.9,0,0,0,0-145.56A8,8,0,1,1,213.71,44,119.58,119.58,0,0,1,248,128ZM53.71,200.78A8,8,0,1,1,42.29,212a119.87,119.87,0,0,1,0-168,8,8,0,1,1,11.42,11.2,103.9,103.9,0,0,0,0,145.56Z"></path>
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
<svg v-else-if="iface.type === 'TCPClientInterface' || iface.type === 'TCPServerInterface' || iface.type === 'UDPInterface'" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 256 256" class="size-6">
|
|
||||||
<path d="M128,24h0A104,104,0,1,0,232,128,104.12,104.12,0,0,0,128,24Zm88,104a87.61,87.61,0,0,1-3.33,24H174.16a157.44,157.44,0,0,0,0-48h38.51A87.61,87.61,0,0,1,216,128ZM102,168H154a115.11,115.11,0,0,1-26,45A115.27,115.27,0,0,1,102,168Zm-3.9-16a140.84,140.84,0,0,1,0-48h59.88a140.84,140.84,0,0,1,0,48ZM40,128a87.61,87.61,0,0,1,3.33-24H81.84a157.44,157.44,0,0,0,0,48H43.33A87.61,87.61,0,0,1,40,128ZM154,88H102a115.11,115.11,0,0,1,26-45A115.27,115.27,0,0,1,154,88Zm52.33,0H170.71a135.28,135.28,0,0,0-22.3-45.6A88.29,88.29,0,0,1,206.37,88ZM107.59,42.4A135.28,135.28,0,0,0,85.29,88H49.63A88.29,88.29,0,0,1,107.59,42.4ZM49.63,168H85.29a135.28,135.28,0,0,0,22.3,45.6A88.29,88.29,0,0,1,49.63,168Zm98.78,45.6a135.28,135.28,0,0,0,22.3-45.6h35.66A88.29,88.29,0,0,1,148.41,213.6Z"></path>
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
<svg v-else-if="iface.type === 'SerialInterface'" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 256 256" class="size-6">
|
|
||||||
<path d="M252.44,121.34l-48-32A8,8,0,0,0,192,96v24H72V72h33a32,32,0,1,0,0-16H72A16,16,0,0,0,56,72v48H8a8,8,0,0,0,0,16H56v48a16,16,0,0,0,16,16h32v8a16,16,0,0,0,16,16h32a16,16,0,0,0,16-16V176a16,16,0,0,0-16-16H120a16,16,0,0,0-16,16v8H72V136H192v24a8,8,0,0,0,12.44,6.66l48-32a8,8,0,0,0,0-13.32ZM136,48a16,16,0,1,1-16,16A16,16,0,0,1,136,48ZM120,176h32v32H120Zm88-30.95V111l25.58,17Z"></path>
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
<svg v-else xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 256 256" class="size-6">
|
|
||||||
<path d="M140,180a12,12,0,1,1-12-12A12,12,0,0,1,140,180ZM128,72c-22.06,0-40,16.15-40,36v4a8,8,0,0,0,16,0v-4c0-11,10.77-20,24-20s24,9,24,20-10.77,20-24,20a8,8,0,0,0-8,8v8a8,8,0,0,0,16,0v-.72c18.24-3.35,32-17.9,32-35.28C168,88.15,150.06,72,128,72Zm104,56A104,104,0,1,1,128,24,104.11,104.11,0,0,1,232,128Zm-16,0a88,88,0,1,0-88,88A88.1,88.1,0,0,0,216,128Z"></path>
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- interface details -->
|
<div class="flex py-2">
|
||||||
<div>
|
|
||||||
<div class="font-semibold leading-5">{{ iface._name }}</div>
|
|
||||||
<div class="text-sm flex space-x-1">
|
|
||||||
|
|
||||||
<!-- auto interface -->
|
<!-- icon -->
|
||||||
<span v-if="iface.type === 'AutoInterface'">
|
<div class="my-auto mx-2">
|
||||||
|
|
||||||
|
<svg v-if="iface.type === 'AutoInterface'" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 256 256" class="size-6">
|
||||||
|
<path d="M219.31,108.68l-80-80a16,16,0,0,0-22.62,0l-80,80A15.87,15.87,0,0,0,32,120v96a8,8,0,0,0,8,8h64a8,8,0,0,0,8-8V160h32v56a8,8,0,0,0,8,8h64a8,8,0,0,0,8-8V120A15.87,15.87,0,0,0,219.31,108.68ZM208,208H160V152a8,8,0,0,0-8-8H104a8,8,0,0,0-8,8v56H48V120l80-80,80,80Z"></path>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<svg v-else-if="iface.type === 'RNodeInterface'" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 256 256" class="size-6">
|
||||||
|
<path d="M128,88a40,40,0,1,0,40,40A40,40,0,0,0,128,88Zm0,64a24,24,0,1,1,24-24A24,24,0,0,1,128,152Zm73.71,7.14a80,80,0,0,1-14.08,22.2,8,8,0,0,1-11.92-10.67,63.95,63.95,0,0,0,0-85.33,8,8,0,1,1,11.92-10.67,80.08,80.08,0,0,1,14.08,84.47ZM69,103.09a64,64,0,0,0,11.26,67.58,8,8,0,0,1-11.92,10.67,79.93,79.93,0,0,1,0-106.67A8,8,0,1,1,80.29,85.34,63.77,63.77,0,0,0,69,103.09ZM248,128a119.58,119.58,0,0,1-34.29,84,8,8,0,1,1-11.42-11.2,103.9,103.9,0,0,0,0-145.56A8,8,0,1,1,213.71,44,119.58,119.58,0,0,1,248,128ZM53.71,200.78A8,8,0,1,1,42.29,212a119.87,119.87,0,0,1,0-168,8,8,0,1,1,11.42,11.2,103.9,103.9,0,0,0,0,145.56Z"></path>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<svg v-else-if="iface.type === 'TCPClientInterface' || iface.type === 'TCPServerInterface' || iface.type === 'UDPInterface'" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 256 256" class="size-6">
|
||||||
|
<path d="M128,24h0A104,104,0,1,0,232,128,104.12,104.12,0,0,0,128,24Zm88,104a87.61,87.61,0,0,1-3.33,24H174.16a157.44,157.44,0,0,0,0-48h38.51A87.61,87.61,0,0,1,216,128ZM102,168H154a115.11,115.11,0,0,1-26,45A115.27,115.27,0,0,1,102,168Zm-3.9-16a140.84,140.84,0,0,1,0-48h59.88a140.84,140.84,0,0,1,0,48ZM40,128a87.61,87.61,0,0,1,3.33-24H81.84a157.44,157.44,0,0,0,0,48H43.33A87.61,87.61,0,0,1,40,128ZM154,88H102a115.11,115.11,0,0,1,26-45A115.27,115.27,0,0,1,154,88Zm52.33,0H170.71a135.28,135.28,0,0,0-22.3-45.6A88.29,88.29,0,0,1,206.37,88ZM107.59,42.4A135.28,135.28,0,0,0,85.29,88H49.63A88.29,88.29,0,0,1,107.59,42.4ZM49.63,168H85.29a135.28,135.28,0,0,0,22.3,45.6A88.29,88.29,0,0,1,49.63,168Zm98.78,45.6a135.28,135.28,0,0,0,22.3-45.6h35.66A88.29,88.29,0,0,1,148.41,213.6Z"></path>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<svg v-else-if="iface.type === 'SerialInterface'" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 256 256" class="size-6">
|
||||||
|
<path d="M252.44,121.34l-48-32A8,8,0,0,0,192,96v24H72V72h33a32,32,0,1,0,0-16H72A16,16,0,0,0,56,72v48H8a8,8,0,0,0,0,16H56v48a16,16,0,0,0,16,16h32v8a16,16,0,0,0,16,16h32a16,16,0,0,0,16-16V176a16,16,0,0,0-16-16H120a16,16,0,0,0-16,16v8H72V136H192v24a8,8,0,0,0,12.44,6.66l48-32a8,8,0,0,0,0-13.32ZM136,48a16,16,0,1,1-16,16A16,16,0,0,1,136,48ZM120,176h32v32H120Zm88-30.95V111l25.58,17Z"></path>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<svg v-else xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 256 256" class="size-6">
|
||||||
|
<path d="M140,180a12,12,0,1,1-12-12A12,12,0,0,1,140,180ZM128,72c-22.06,0-40,16.15-40,36v4a8,8,0,0,0,16,0v-4c0-11,10.77-20,24-20s24,9,24,20-10.77,20-24,20a8,8,0,0,0-8,8v8a8,8,0,0,0,16,0v-.72c18.24-3.35,32-17.9,32-35.28C168,88.15,150.06,72,128,72Zm104,56A104,104,0,1,1,128,24,104.11,104.11,0,0,1,232,128Zm-16,0a88,88,0,1,0-88,88A88.1,88.1,0,0,0,216,128Z"></path>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- interface details -->
|
||||||
|
<div>
|
||||||
|
<div class="font-semibold leading-5">{{ iface._name }}</div>
|
||||||
|
<div class="text-sm flex space-x-1">
|
||||||
|
|
||||||
|
<!-- auto interface -->
|
||||||
|
<span v-if="iface.type === 'AutoInterface'">
|
||||||
{{ iface.type }} • Ethernet and WiFi
|
{{ iface.type }} • Ethernet and WiFi
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!-- tcp client interface -->
|
<!-- tcp client interface -->
|
||||||
<span v-else-if="iface.type === 'TCPClientInterface'">
|
<span v-else-if="iface.type === 'TCPClientInterface'">
|
||||||
{{ iface.type }} • {{ iface.target_host }}:{{ iface.target_port }}
|
{{ iface.type }} • {{ iface.target_host }}:{{ iface.target_port }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!-- tcp server interface -->
|
<!-- tcp server interface -->
|
||||||
<span v-else-if="iface.type === 'TCPServerInterface'">
|
<span v-else-if="iface.type === 'TCPServerInterface'">
|
||||||
{{ iface.type }} • {{ iface.listen_ip }}:{{ iface.listen_port }}
|
{{ iface.type }} • {{ iface.listen_ip }}:{{ iface.listen_port }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!-- udp interface -->
|
<!-- udp interface -->
|
||||||
<span v-else-if="iface.type === 'UDPInterface'">
|
<span v-else-if="iface.type === 'UDPInterface'">
|
||||||
{{ iface.type }} • {{ iface.listen_ip }}:{{ iface.listen_port }} • {{ iface.forward_ip }}:{{ iface.forward_port }}
|
{{ iface.type }} • {{ iface.listen_ip }}:{{ iface.listen_port }} • {{ iface.forward_ip }}:{{ iface.forward_port }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!-- rnode interface details -->
|
<!-- rnode interface details -->
|
||||||
<span v-else-if="iface.type === 'RNodeInterface'">
|
<span v-else-if="iface.type === 'RNodeInterface'">
|
||||||
{{ iface.type }} • {{ iface.port }} • freq={{ iface.frequency }} • bw={{ iface.bandwidth }} • power={{ iface.txpower }}dBm • sf={{ iface.spreadingfactor }} • cr={{ iface.codingrate }}
|
{{ iface.type }} • {{ iface.port }} • freq={{ iface.frequency }} • bw={{ iface.bandwidth }} • power={{ iface.txpower }}dBm • sf={{ iface.spreadingfactor }} • cr={{ iface.codingrate }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!-- unknown interface types -->
|
<!-- unknown interface types -->
|
||||||
<span v-else>
|
<span v-else>
|
||||||
{{ iface.type ?? 'Unknown Interface Type' }}
|
{{ iface.type ?? 'Unknown Interface Type' }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- enabled state badge -->
|
<!-- enabled state badge -->
|
||||||
<div class="ml-auto my-auto mr-2">
|
<div class="ml-auto my-auto mr-2">
|
||||||
<span v-if="isInterfaceEnabled(iface)" class="inline-flex items-center rounded-full bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-600/20">Enabled</span>
|
<span v-if="isInterfaceEnabled(iface)" class="inline-flex items-center rounded-full bg-green-50 px-2 py-1 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-600/20">Enabled</span>
|
||||||
<span v-else class="inline-flex items-center rounded-full bg-red-50 px-2 py-1 text-xs font-medium text-red-700 ring-1 ring-inset ring-red-600/20">Disabled</span>
|
<span v-else class="inline-flex items-center rounded-full bg-red-50 px-2 py-1 text-xs font-medium text-red-700 ring-1 ring-inset ring-red-600/20">Disabled</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- enable/disable interface button -->
|
<!-- enable/disable interface button -->
|
||||||
<div class="my-auto mr-1">
|
<div class="my-auto mr-1">
|
||||||
<button v-if="isInterfaceEnabled(iface)" @click="disableInterface(iface._name)" type="button" class="cursor-pointer">
|
<button v-if="isInterfaceEnabled(iface)" @click="disableInterface(iface._name)" type="button" class="cursor-pointer">
|
||||||
<span class="flex text-gray-700 bg-gray-100 hover:bg-gray-200 p-2 rounded-full">
|
<span class="flex text-gray-700 bg-gray-100 hover:bg-gray-200 p-2 rounded-full">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-5">
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-5">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5.636 5.636a9 9 0 1 0 12.728 0M12 3v9" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M5.636 5.636a9 9 0 1 0 12.728 0M12 3v9" />
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<button v-else @click="enableInterface(iface._name)" type="button" class="cursor-pointer">
|
<button v-else @click="enableInterface(iface._name)" type="button" class="cursor-pointer">
|
||||||
<span class="flex text-gray-700 bg-gray-100 hover:bg-gray-200 p-2 rounded-full">
|
<span class="flex text-gray-700 bg-gray-100 hover:bg-gray-200 p-2 rounded-full">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-5">
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-5">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5.636 5.636a9 9 0 1 0 12.728 0M12 3v9" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M5.636 5.636a9 9 0 1 0 12.728 0M12 3v9" />
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- edit interface button -->
|
<!-- edit interface button -->
|
||||||
<div class="my-auto mr-1">
|
<div class="my-auto mr-1">
|
||||||
<button @click="editInterface(iface._name)" type="button" class="cursor-pointer">
|
<button @click="editInterface(iface._name)" type="button" class="cursor-pointer">
|
||||||
<span class="flex text-gray-700 bg-gray-100 hover:bg-gray-200 p-2 rounded-full">
|
<span class="flex text-gray-700 bg-gray-100 hover:bg-gray-200 p-2 rounded-full">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-5">
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-5">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125" />
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- delete interface button -->
|
<!-- delete interface button -->
|
||||||
<div class="my-auto mr-2">
|
<div class="my-auto mr-2">
|
||||||
<button @click="deleteInterface(iface._name)" type="button" class="cursor-pointer">
|
<button @click="deleteInterface(iface._name)" type="button" class="cursor-pointer">
|
||||||
<span class="flex text-gray-700 bg-gray-100 hover:bg-gray-200 p-2 rounded-full">
|
<span class="flex text-gray-700 bg-gray-100 hover:bg-gray-200 p-2 rounded-full">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex bg-gray-50 p-1 text-sm text-gray-500 space-x-1 border-t">
|
|
||||||
|
|
||||||
<!-- status -->
|
|
||||||
<div v-if="iface._stats?.status === true" class="text-sm text-green-500">Connected</div>
|
|
||||||
<div v-else class="text-sm text-red-500">Disconnected</div>
|
|
||||||
|
|
||||||
<!-- stats -->
|
|
||||||
<div>• Bitrate: {{ formatBitsPerSecond(iface._stats?.bitrate ?? 0) }}</div>
|
|
||||||
<div>• TX: {{ formatBytes(iface._stats?.txb ?? 0) }}</div>
|
|
||||||
<div>• RX: {{ formatBytes(iface._stats?.rxb ?? 0) }}</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- add interface tab -->
|
|
||||||
<div v-if="tab === 'interfaces.add'" class="overflow-y-auto p-2 space-y-2">
|
|
||||||
|
|
||||||
<!-- community interfaces -->
|
|
||||||
<div v-if="!isEditingInterface && config != null && config.show_suggested_community_interfaces" class="bg-white rounded shadow divide-y divide-gray-200">
|
|
||||||
<div class="flex p-2">
|
|
||||||
<div class="my-auto mr-auto">
|
|
||||||
<div class="font-bold">Community Interfaces</div>
|
|
||||||
<div class="text-sm">These TCP interfaces serve as a quick way to test Reticulum. We suggest running your own as these may not always be available.</div>
|
|
||||||
</div>
|
|
||||||
<div class="my-auto ml-2">
|
|
||||||
<button @click="updateConfig({'show_suggested_community_interfaces': false})" type="button" class="text-gray-700 bg-gray-100 hover:bg-gray-200 p-2 rounded-full">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
|
|
||||||
<path d="M6.28 5.22a.75.75 0 0 0-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 1 0 1.06 1.06L10 11.06l3.72 3.72a.75.75 0 1 0 1.06-1.06L11.06 10l3.72-3.72a.75.75 0 0 0-1.06-1.06L10 8.94 6.28 5.22Z" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="divide-y divide-gray-200">
|
|
||||||
|
|
||||||
<div class="flex px-2 py-1">
|
|
||||||
<div class="my-auto mr-auto">
|
|
||||||
<div>RNS Testnet Amsterdam</div>
|
|
||||||
<div class="text-xs">amsterdam.connect.reticulum.network:4965</div>
|
|
||||||
</div>
|
|
||||||
<div class="ml-2 my-auto">
|
|
||||||
<button @click="newInterfaceName='RNS Testnet Amsterdam';newInterfaceType='TCPClientInterface';newInterfaceTargetHost='amsterdam.connect.reticulum.network';newInterfaceTargetPort='4965'" type="button" class="inline-flex items-center gap-x-1 rounded-md bg-gray-500 px-2 py-1 text-sm font-semibold text-white shadow-sm hover:bg-gray-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-500">
|
|
||||||
<span>Use Interface</span>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex px-2 py-1">
|
<div class="flex bg-gray-50 p-1 text-sm text-gray-500 space-x-1 border-t">
|
||||||
<div class="my-auto mr-auto">
|
|
||||||
<div>RNS Testnet BetweenTheBorders</div>
|
<!-- status -->
|
||||||
<div class="text-xs">betweentheborders.com:4242</div>
|
<div v-if="iface._stats?.status === true" class="text-sm text-green-500">Connected</div>
|
||||||
</div>
|
<div v-else class="text-sm text-red-500">Disconnected</div>
|
||||||
<div class="ml-2 my-auto">
|
|
||||||
<button @click="newInterfaceName='RNS Testnet BetweenTheBorders';newInterfaceType='TCPClientInterface';newInterfaceTargetHost='betweentheborders.com';newInterfaceTargetPort='4242'" type="button" class="inline-flex items-center gap-x-1 rounded-md bg-gray-500 px-2 py-1 text-sm font-semibold text-white shadow-sm hover:bg-gray-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-500">
|
<!-- stats -->
|
||||||
<span>Use Interface</span>
|
<div>• Bitrate: {{ formatBitsPerSecond(iface._stats?.bitrate ?? 0) }}</div>
|
||||||
</button>
|
<div>• TX: {{ formatBytes(iface._stats?.txb ?? 0) }}</div>
|
||||||
</div>
|
<div>• RX: {{ formatBytes(iface._stats?.rxb ?? 0) }}</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- add interface form -->
|
<!-- add interface tab -->
|
||||||
<div class="bg-white rounded shadow divide-y divide-gray-200">
|
<div v-if="tab === 'interfaces.add'" class="overflow-y-auto p-2 space-y-2">
|
||||||
<div class="p-2 font-bold">
|
|
||||||
<span v-if="isEditingInterface">Edit Interface</span>
|
|
||||||
<span v-else>Add Interface</span>
|
|
||||||
</div>
|
|
||||||
<div class="p-2 space-y-3">
|
|
||||||
|
|
||||||
<!-- interface name -->
|
<!-- community interfaces -->
|
||||||
<div>
|
<div v-if="!isEditingInterface && config != null && config.show_suggested_community_interfaces" class="bg-white rounded shadow divide-y divide-gray-200">
|
||||||
<label class="block mb-2 text-sm font-medium text-gray-900">Name</label>
|
<div class="flex p-2">
|
||||||
<input type="text" :disabled="isEditingInterface" placeholder="New Interface Name" v-model="newInterfaceName" class="border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5" :class="[ isEditingInterface ? 'cursor-not-allowed bg-gray-200' : 'bg-gray-50' ]">
|
<div class="my-auto mr-auto">
|
||||||
<div class="text-xs text-gray-600">Interface names must be unique.</div>
|
<div class="font-bold">Community Interfaces</div>
|
||||||
</div>
|
<div class="text-sm">These TCP interfaces serve as a quick way to test Reticulum. We suggest running your own as these may not always be available.</div>
|
||||||
|
</div>
|
||||||
<!-- interface type -->
|
<div class="my-auto ml-2">
|
||||||
<div class="mb-2">
|
<button @click="updateConfig({'show_suggested_community_interfaces': false})" type="button" class="text-gray-700 bg-gray-100 hover:bg-gray-200 p-2 rounded-full">
|
||||||
<label class="block mb-2 text-sm font-medium text-gray-900">Type</label>
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
|
||||||
<select v-model="newInterfaceType" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5">
|
<path d="M6.28 5.22a.75.75 0 0 0-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 1 0 1.06 1.06L10 11.06l3.72 3.72a.75.75 0 1 0 1.06-1.06L11.06 10l3.72-3.72a.75.75 0 0 0-1.06-1.06L10 8.94 6.28 5.22Z" />
|
||||||
<option disabled selected>--</option>
|
</svg>
|
||||||
<option value="AutoInterface">AutoInterface</option>
|
</button>
|
||||||
<option value="RNodeInterface">RNodeInterface</option>
|
|
||||||
<option value="TCPClientInterface">TCPClientInterface</option>
|
|
||||||
<option value="TCPServerInterface">TCPServerInterface</option>
|
|
||||||
<option value="UDPInterface">UDPInterface</option>
|
|
||||||
</select>
|
|
||||||
<div class="text-xs text-gray-600">
|
|
||||||
Need help? <a class="text-blue-500 underline" href="https://reticulum.network/manual/interfaces.html" target="_blank">Reticulum Docs: Configuring Interfaces</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="divide-y divide-gray-200">
|
||||||
|
|
||||||
|
<div class="flex px-2 py-1">
|
||||||
|
<div class="my-auto mr-auto">
|
||||||
|
<div>RNS Testnet Amsterdam</div>
|
||||||
|
<div class="text-xs">amsterdam.connect.reticulum.network:4965</div>
|
||||||
|
</div>
|
||||||
|
<div class="ml-2 my-auto">
|
||||||
|
<button @click="newInterfaceName='RNS Testnet Amsterdam';newInterfaceType='TCPClientInterface';newInterfaceTargetHost='amsterdam.connect.reticulum.network';newInterfaceTargetPort='4965'" type="button" class="inline-flex items-center gap-x-1 rounded-md bg-gray-500 px-2 py-1 text-sm font-semibold text-white shadow-sm hover:bg-gray-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-500">
|
||||||
|
<span>Use Interface</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex px-2 py-1">
|
||||||
|
<div class="my-auto mr-auto">
|
||||||
|
<div>RNS Testnet BetweenTheBorders</div>
|
||||||
|
<div class="text-xs">betweentheborders.com:4242</div>
|
||||||
|
</div>
|
||||||
|
<div class="ml-2 my-auto">
|
||||||
|
<button @click="newInterfaceName='RNS Testnet BetweenTheBorders';newInterfaceType='TCPClientInterface';newInterfaceTargetHost='betweentheborders.com';newInterfaceTargetPort='4242'" type="button" class="inline-flex items-center gap-x-1 rounded-md bg-gray-500 px-2 py-1 text-sm font-semibold text-white shadow-sm hover:bg-gray-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-500">
|
||||||
|
<span>Use Interface</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- interface target host -->
|
|
||||||
<div v-if="newInterfaceType === 'TCPClientInterface'" class="mb-2">
|
|
||||||
<label class="block mb-2 text-sm font-medium text-gray-900">Target Host</label>
|
|
||||||
<input type="text" placeholder="example.com" v-model="newInterfaceTargetHost" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5">
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- interface target port -->
|
<!-- add interface form -->
|
||||||
<div v-if="newInterfaceType === 'TCPClientInterface'" class="mb-2">
|
<div class="bg-white rounded shadow divide-y divide-gray-200">
|
||||||
<label class="block mb-2 text-sm font-medium text-gray-900">Target Port</label>
|
<div class="p-2 font-bold">
|
||||||
<input type="text" placeholder="1234" v-model="newInterfaceTargetPort" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5">
|
<span v-if="isEditingInterface">Edit Interface</span>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- interface listen ip -->
|
|
||||||
<div v-if="newInterfaceType === 'TCPServerInterface' || newInterfaceType === 'UDPInterface'" class="mb-2">
|
|
||||||
<label class="block mb-2 text-sm font-medium text-gray-900">Listen IP</label>
|
|
||||||
<input type="text" placeholder="0.0.0.0" v-model="newInterfaceListenIp" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- interface listen port -->
|
|
||||||
<div v-if="newInterfaceType === 'TCPServerInterface' || newInterfaceType === 'UDPInterface'" class="mb-2">
|
|
||||||
<label class="block mb-2 text-sm font-medium text-gray-900">Listen Port</label>
|
|
||||||
<input type="text" placeholder="1234" v-model="newInterfaceListenPort" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- interface forward ip -->
|
|
||||||
<div v-if="newInterfaceType === 'UDPInterface'" class="mb-2">
|
|
||||||
<label class="block mb-2 text-sm font-medium text-gray-900">Forward IP</label>
|
|
||||||
<input type="text" placeholder="255.255.255.255" v-model="newInterfaceForwardIp" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- interface listen port -->
|
|
||||||
<div v-if="newInterfaceType === 'UDPInterface'" class="mb-2">
|
|
||||||
<label class="block mb-2 text-sm font-medium text-gray-900">Forward Port</label>
|
|
||||||
<input type="text" placeholder="1234" v-model="newInterfaceForwardPort" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- interface port -->
|
|
||||||
<div v-if="newInterfaceType === 'RNodeInterface'" class="mb-2">
|
|
||||||
<label class="block mb-2 text-sm font-medium text-gray-900">Port</label>
|
|
||||||
<select v-model="newInterfacePort" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5">
|
|
||||||
<option v-for="comport of comports" :value="comport.device">{{ comport.device }} (Product: {{ comport.product ?? '?' }}, Serial: {{ comport.serial ?? '?' }})</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- interface frequency -->
|
|
||||||
<div v-if="newInterfaceType === 'RNodeInterface'" class="mb-2">
|
|
||||||
<label class="block mb-2 text-sm font-medium text-gray-900">Frequency (Hz)</label>
|
|
||||||
<input type="number" v-model="newInterfaceFrequency" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5">
|
|
||||||
<div class="text-xs text-gray-600">{{ formatFrequency(newInterfaceFrequency) }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- interface bandwidth -->
|
|
||||||
<div v-if="newInterfaceType === 'RNodeInterface'" class="mb-2">
|
|
||||||
<label class="block mb-2 text-sm font-medium text-gray-900">Bandwidth</label>
|
|
||||||
<select v-model="newInterfaceBandwidth" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5">
|
|
||||||
<option v-for="bandwidth in RNodeInterfaceDefaults.bandwidths" :value="bandwidth">{{ bandwidth / 1000 }} KHz</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- interface txpower -->
|
|
||||||
<div v-if="newInterfaceType === 'RNodeInterface'" class="mb-2">
|
|
||||||
<label class="block mb-2 text-sm font-medium text-gray-900">Transmit Power (dBm)</label>
|
|
||||||
<input v-model="newInterfaceTxpower" type="number" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- interface spreading factor -->
|
|
||||||
<div v-if="newInterfaceType === 'RNodeInterface'" class="mb-2">
|
|
||||||
<label class="block mb-2 text-sm font-medium text-gray-900">Spreading Factor</label>
|
|
||||||
<select v-model="newInterfaceSpreadingFactor" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5">
|
|
||||||
<option v-for="spreadingfactor in RNodeInterfaceDefaults.spreadingfactors" :value="spreadingfactor">{{ spreadingfactor }}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- interface coding rate -->
|
|
||||||
<div v-if="newInterfaceType === 'RNodeInterface'" class="mb-2">
|
|
||||||
<label class="block mb-2 text-sm font-medium text-gray-900">Coding Rate</label>
|
|
||||||
<select v-model="newInterfaceCodingRate" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5">
|
|
||||||
<option v-for="codingrate in RNodeInterfaceDefaults.codingrates" :value="codingrate">4:{{ codingrate }}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- add button -->
|
|
||||||
<button @click="addInterface" type="button" class="bg-green-500 hover:bg-green-400 focus-visible:outline-green-500 my-auto inline-flex items-center gap-x-1 rounded-md p-2 text-sm font-semibold text-white shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2">
|
|
||||||
<span v-if="isEditingInterface">Save Interface</span>
|
|
||||||
<span v-else>Add Interface</span>
|
<span v-else>Add Interface</span>
|
||||||
</button>
|
</div>
|
||||||
|
<div class="p-2 space-y-3">
|
||||||
|
|
||||||
|
<!-- interface name -->
|
||||||
|
<div>
|
||||||
|
<label class="block mb-2 text-sm font-medium text-gray-900">Name</label>
|
||||||
|
<input type="text" :disabled="isEditingInterface" placeholder="New Interface Name" v-model="newInterfaceName" class="border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5" :class="[ isEditingInterface ? 'cursor-not-allowed bg-gray-200' : 'bg-gray-50' ]">
|
||||||
|
<div class="text-xs text-gray-600">Interface names must be unique.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- interface type -->
|
||||||
|
<div class="mb-2">
|
||||||
|
<label class="block mb-2 text-sm font-medium text-gray-900">Type</label>
|
||||||
|
<select v-model="newInterfaceType" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5">
|
||||||
|
<option disabled selected>--</option>
|
||||||
|
<option value="AutoInterface">AutoInterface</option>
|
||||||
|
<option value="RNodeInterface">RNodeInterface</option>
|
||||||
|
<option value="TCPClientInterface">TCPClientInterface</option>
|
||||||
|
<option value="TCPServerInterface">TCPServerInterface</option>
|
||||||
|
<option value="UDPInterface">UDPInterface</option>
|
||||||
|
</select>
|
||||||
|
<div class="text-xs text-gray-600">
|
||||||
|
Need help? <a class="text-blue-500 underline" href="https://reticulum.network/manual/interfaces.html" target="_blank">Reticulum Docs: Configuring Interfaces</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- interface target host -->
|
||||||
|
<div v-if="newInterfaceType === 'TCPClientInterface'" class="mb-2">
|
||||||
|
<label class="block mb-2 text-sm font-medium text-gray-900">Target Host</label>
|
||||||
|
<input type="text" placeholder="example.com" v-model="newInterfaceTargetHost" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- interface target port -->
|
||||||
|
<div v-if="newInterfaceType === 'TCPClientInterface'" class="mb-2">
|
||||||
|
<label class="block mb-2 text-sm font-medium text-gray-900">Target Port</label>
|
||||||
|
<input type="text" placeholder="1234" v-model="newInterfaceTargetPort" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- interface listen ip -->
|
||||||
|
<div v-if="newInterfaceType === 'TCPServerInterface' || newInterfaceType === 'UDPInterface'" class="mb-2">
|
||||||
|
<label class="block mb-2 text-sm font-medium text-gray-900">Listen IP</label>
|
||||||
|
<input type="text" placeholder="0.0.0.0" v-model="newInterfaceListenIp" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- interface listen port -->
|
||||||
|
<div v-if="newInterfaceType === 'TCPServerInterface' || newInterfaceType === 'UDPInterface'" class="mb-2">
|
||||||
|
<label class="block mb-2 text-sm font-medium text-gray-900">Listen Port</label>
|
||||||
|
<input type="text" placeholder="1234" v-model="newInterfaceListenPort" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- interface forward ip -->
|
||||||
|
<div v-if="newInterfaceType === 'UDPInterface'" class="mb-2">
|
||||||
|
<label class="block mb-2 text-sm font-medium text-gray-900">Forward IP</label>
|
||||||
|
<input type="text" placeholder="255.255.255.255" v-model="newInterfaceForwardIp" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- interface listen port -->
|
||||||
|
<div v-if="newInterfaceType === 'UDPInterface'" class="mb-2">
|
||||||
|
<label class="block mb-2 text-sm font-medium text-gray-900">Forward Port</label>
|
||||||
|
<input type="text" placeholder="1234" v-model="newInterfaceForwardPort" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- interface port -->
|
||||||
|
<div v-if="newInterfaceType === 'RNodeInterface'" class="mb-2">
|
||||||
|
<label class="block mb-2 text-sm font-medium text-gray-900">Port</label>
|
||||||
|
<select v-model="newInterfacePort" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5">
|
||||||
|
<option v-for="comport of comports" :value="comport.device">{{ comport.device }} (Product: {{ comport.product ?? '?' }}, Serial: {{ comport.serial ?? '?' }})</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- interface frequency -->
|
||||||
|
<div v-if="newInterfaceType === 'RNodeInterface'" class="mb-2">
|
||||||
|
<label class="block mb-2 text-sm font-medium text-gray-900">Frequency (Hz)</label>
|
||||||
|
<input type="number" v-model="newInterfaceFrequency" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5">
|
||||||
|
<div class="text-xs text-gray-600">{{ formatFrequency(newInterfaceFrequency) }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- interface bandwidth -->
|
||||||
|
<div v-if="newInterfaceType === 'RNodeInterface'" class="mb-2">
|
||||||
|
<label class="block mb-2 text-sm font-medium text-gray-900">Bandwidth</label>
|
||||||
|
<select v-model="newInterfaceBandwidth" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5">
|
||||||
|
<option v-for="bandwidth in RNodeInterfaceDefaults.bandwidths" :value="bandwidth">{{ bandwidth / 1000 }} KHz</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- interface txpower -->
|
||||||
|
<div v-if="newInterfaceType === 'RNodeInterface'" class="mb-2">
|
||||||
|
<label class="block mb-2 text-sm font-medium text-gray-900">Transmit Power (dBm)</label>
|
||||||
|
<input v-model="newInterfaceTxpower" type="number" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- interface spreading factor -->
|
||||||
|
<div v-if="newInterfaceType === 'RNodeInterface'" class="mb-2">
|
||||||
|
<label class="block mb-2 text-sm font-medium text-gray-900">Spreading Factor</label>
|
||||||
|
<select v-model="newInterfaceSpreadingFactor" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5">
|
||||||
|
<option v-for="spreadingfactor in RNodeInterfaceDefaults.spreadingfactors" :value="spreadingfactor">{{ spreadingfactor }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- interface coding rate -->
|
||||||
|
<div v-if="newInterfaceType === 'RNodeInterface'" class="mb-2">
|
||||||
|
<label class="block mb-2 text-sm font-medium text-gray-900">Coding Rate</label>
|
||||||
|
<select v-model="newInterfaceCodingRate" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5">
|
||||||
|
<option v-for="codingrate in RNodeInterfaceDefaults.codingrates" :value="codingrate">4:{{ codingrate }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- add button -->
|
||||||
|
<button @click="addInterface" type="button" class="bg-green-500 hover:bg-green-400 focus-visible:outline-green-500 my-auto inline-flex items-center gap-x-1 rounded-md p-2 text-sm font-semibold text-white shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2">
|
||||||
|
<span v-if="isEditingInterface">Save Interface</span>
|
||||||
|
<span v-else>Add Interface</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
616
src/frontend/components/nomadnetwork/NomadNetworkPage.vue
Normal file
616
src/frontend/components/nomadnetwork/NomadNetworkPage.vue
Normal file
@@ -0,0 +1,616 @@
|
|||||||
|
<template>
|
||||||
|
|
||||||
|
<!-- nomadnetwork sidebar -->
|
||||||
|
<NomadNetworkSidebar
|
||||||
|
:nodes="nodes"
|
||||||
|
:selected-destination-hash="selectedNode?.destination_hash"
|
||||||
|
@node-click="onNodeClick"/>
|
||||||
|
|
||||||
|
<div class="flex flex-col flex-1 overflow-hidden min-w-full sm:min-w-[500px]">
|
||||||
|
|
||||||
|
<!-- node -->
|
||||||
|
<div v-if="selectedNode" class="m-2 flex flex-col h-full border rounded-xl bg-white shadow overflow-hidden">
|
||||||
|
|
||||||
|
<!-- header -->
|
||||||
|
<div class="flex p-2 border-b border-gray-300">
|
||||||
|
|
||||||
|
<!-- node info -->
|
||||||
|
<div class="my-auto">
|
||||||
|
<span class="font-semibold">{{ selectedNode.name }}</span>
|
||||||
|
<span v-if="selectedNodePath" @click="onDestinationPathClick(selectedNodePath)" class="text-sm cursor-pointer"> - {{ selectedNodePath.hops }} {{ selectedNodePath.hops === 1 ? 'hop' : 'hops' }} away</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- close button -->
|
||||||
|
<div class="my-auto ml-auto mr-2">
|
||||||
|
<div @click="selectedNode = null" class="cursor-pointer">
|
||||||
|
<div class="flex text-gray-700 bg-gray-100 hover:bg-gray-200 p-1 rounded-full">
|
||||||
|
<div>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
|
||||||
|
<path d="M6.28 5.22a.75.75 0 0 0-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 1 0 1.06 1.06L10 11.06l3.72 3.72a.75.75 0 1 0 1.06-1.06L11.06 10l3.72-3.72a.75.75 0 0 0-1.06-1.06L10 8.94 6.28 5.22Z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- browser navigation -->
|
||||||
|
<div class="flex w-full border-gray-300 border-b p-2">
|
||||||
|
<button @click="loadNodePage(selectedNode.destination_hash, '/page/index.mu')" type="button" class="my-auto text-gray-500 bg-gray-200 hover:bg-gray-300 rounded p-1 cursor-pointer">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
|
||||||
|
<path fill-rule="evenodd" d="M9.293 2.293a1 1 0 0 1 1.414 0l7 7A1 1 0 0 1 17 11h-1v6a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1v-3a1 1 0 0 0-1-1H9a1 1 0 0 0-1 1v3a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-6H3a1 1 0 0 1-.707-1.707l7-7Z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button @click="reloadNodePage" type="button" class="ml-1 my-auto text-gray-500 bg-gray-200 hover:bg-gray-300 rounded p-1 cursor-pointer">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
|
||||||
|
<path fill-rule="evenodd" d="M15.312 11.424a5.5 5.5 0 0 1-9.201 2.466l-.312-.311h2.433a.75.75 0 0 0 0-1.5H3.989a.75.75 0 0 0-.75.75v4.242a.75.75 0 0 0 1.5 0v-2.43l.31.31a7 7 0 0 0 11.712-3.138.75.75 0 0 0-1.449-.39Zm1.23-3.723a.75.75 0 0 0 .219-.53V2.929a.75.75 0 0 0-1.5 0V5.36l-.31-.31A7 7 0 0 0 3.239 8.188a.75.75 0 1 0 1.448.389A5.5 5.5 0 0 1 13.89 6.11l.311.31h-2.432a.75.75 0 0 0 0 1.5h4.243a.75.75 0 0 0 .53-.219Z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button @click="loadPreviousNodePage" type="button" :disabled="nodePagePathHistory.length === 0" :class="[ nodePagePathHistory.length > 0 ? 'text-gray-500 bg-gray-200 hover:bg-gray-300' : 'text-gray-400 bg-gray-100']" class="ml-1 my-auto rounded p-1 cursor-pointer">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
|
||||||
|
<path fill-rule="evenodd" d="M17 10a.75.75 0 0 1-.75.75H5.612l4.158 3.96a.75.75 0 1 1-1.04 1.08l-5.5-5.25a.75.75 0 0 1 0-1.08l5.5-5.25a.75.75 0 1 1 1.04 1.08L5.612 9.25H16.25A.75.75 0 0 1 17 10Z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div class="my-auto mx-2 w-full">
|
||||||
|
<input v-model="nodePagePathUrlInput" @keyup.enter="onNodePageUrlClick(nodePagePathUrlInput)" type="text" placeholder="Enter Destination URL" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full px-2.5 py-1.5">
|
||||||
|
</div>
|
||||||
|
<button @click="onNodePageUrlClick(nodePagePathUrlInput)" type="button" class="my-auto text-gray-500 bg-gray-200 hover:bg-gray-300 rounded p-1 cursor-pointer">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
|
||||||
|
<path fill-rule="evenodd" d="M3 10a.75.75 0 0 1 .75-.75h10.638L10.23 5.29a.75.75 0 1 1 1.04-1.08l5.5 5.25a.75.75 0 0 1 0 1.08l-5.5 5.25a.75.75 0 1 1-1.04-1.08l4.158-3.96H3.75A.75.75 0 0 1 3 10Z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- page content -->
|
||||||
|
<div class="h-full overflow-y-scroll p-3 bg-black text-white">
|
||||||
|
<div class="flex" v-if="isLoadingNodePage">
|
||||||
|
<div class="my-auto">
|
||||||
|
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||||
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="my-auto">Loading {{ nodePageProgress }}%</div>
|
||||||
|
</div>
|
||||||
|
<pre v-else v-html="nodePageContent" class="h-full text-wrap"></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- file download bottom bar -->
|
||||||
|
<div v-if="isDownloadingNodeFile" class="flex w-full border-gray-300 border-t p-2">
|
||||||
|
<div class="my-auto mr-2">
|
||||||
|
<svg class="animate-spin h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||||
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="my-auto">Downloading: {{ nodeFilePath }} ({{ nodeFileProgress }}%)</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- no node selected -->
|
||||||
|
<div v-else class="flex flex-col mx-auto my-auto text-center leading-5">
|
||||||
|
<div class="mx-auto mb-1">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 21a9.004 9.004 0 0 0 8.716-6.747M12 21a9.004 9.004 0 0 1-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 0 1 7.843 4.582M12 3a8.997 8.997 0 0 0-7.843 4.582m15.686 0A11.953 11.953 0 0 1 12 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0 1 21 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0 1 12 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 0 1 3 12c0-1.605.42-3.113 1.157-4.418" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="font-semibold">No Active Node</div>
|
||||||
|
<div>Select a Node to start browsing!</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
import DialogUtils from "../../js/DialogUtils";
|
||||||
|
import Utils from "../../js/Utils";
|
||||||
|
import WebSocketConnection from "../../js/WebSocketConnection";
|
||||||
|
import NomadNetworkSidebar from "./NomadNetworkSidebar.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'NomadNetworkPage',
|
||||||
|
components: {
|
||||||
|
NomadNetworkSidebar,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
|
||||||
|
nodes: {},
|
||||||
|
selectedNode: null,
|
||||||
|
selectedNodePath: null,
|
||||||
|
|
||||||
|
isLoadingNodePage: false,
|
||||||
|
nodePageRequestSequence: 0,
|
||||||
|
nodePagePath: null,
|
||||||
|
nodePagePathUrlInput: null,
|
||||||
|
nodePageContent: null,
|
||||||
|
nodePageProgress: 0,
|
||||||
|
nodePagePathHistory: [],
|
||||||
|
nodePageCache: {},
|
||||||
|
|
||||||
|
isDownloadingNodeFile: false,
|
||||||
|
nodeFilePath: null,
|
||||||
|
nodeFileProgress: 0,
|
||||||
|
|
||||||
|
nomadnetPageDownloadCallbacks: {},
|
||||||
|
nomadnetFileDownloadCallbacks: {},
|
||||||
|
|
||||||
|
};
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
// listen for websocket messages
|
||||||
|
WebSocketConnection.on("message", this.onWebsocketMessage);
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
// stop listening for websocket messages
|
||||||
|
WebSocketConnection.off("message", this.onWebsocketMessage);
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.getNomadnetworkNodeAnnounces();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async onWebsocketMessage(message) {
|
||||||
|
const json = JSON.parse(message.data);
|
||||||
|
switch(json.type){
|
||||||
|
case 'announce': {
|
||||||
|
const aspect = json.announce.aspect;
|
||||||
|
if(aspect === "nomadnetwork.node"){
|
||||||
|
this.updateNodeFromAnnounce(json.announce);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'nomadnet.page.download': {
|
||||||
|
|
||||||
|
// get data from server
|
||||||
|
const nomadnetPageDownload = json.nomadnet_page_download;
|
||||||
|
|
||||||
|
// find download callbacks
|
||||||
|
const getNomadnetPageDownloadCallbackKey = this.getNomadnetPageDownloadCallbackKey(nomadnetPageDownload.destination_hash, nomadnetPageDownload.page_path);
|
||||||
|
const nomadnetPageDownloadCallback = this.nomadnetPageDownloadCallbacks[getNomadnetPageDownloadCallbackKey];
|
||||||
|
if(!nomadnetPageDownloadCallback){
|
||||||
|
console.log("did not find nomadnet page download callback for key: " + getNomadnetPageDownloadCallbackKey);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle success
|
||||||
|
if(nomadnetPageDownload.status === "success" && nomadnetPageDownloadCallback.onSuccessCallback){
|
||||||
|
nomadnetPageDownloadCallback.onSuccessCallback(nomadnetPageDownload.page_content);
|
||||||
|
delete this.nomadnetPageDownloadCallbacks[getNomadnetPageDownloadCallbackKey];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle failure
|
||||||
|
if(nomadnetPageDownload.status === "failure" && nomadnetPageDownloadCallback.onFailureCallback){
|
||||||
|
nomadnetPageDownloadCallback.onFailureCallback(nomadnetPageDownload.failure_reason);
|
||||||
|
delete this.nomadnetPageDownloadCallbacks[getNomadnetPageDownloadCallbackKey];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle progress
|
||||||
|
if(nomadnetPageDownload.status === "progress" && nomadnetPageDownloadCallback.onProgressCallback){
|
||||||
|
nomadnetPageDownloadCallback.onProgressCallback(nomadnetPageDownload.progress);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
}
|
||||||
|
case 'nomadnet.file.download': {
|
||||||
|
|
||||||
|
// get data from server
|
||||||
|
const nomadnetFileDownload = json.nomadnet_file_download;
|
||||||
|
|
||||||
|
// find download callbacks
|
||||||
|
const getNomadnetFileDownloadCallbackKey = this.getNomadnetFileDownloadCallbackKey(nomadnetFileDownload.destination_hash, nomadnetFileDownload.file_path);
|
||||||
|
const nomadnetFileDownloadCallback = this.nomadnetFileDownloadCallbacks[getNomadnetFileDownloadCallbackKey];
|
||||||
|
if(!nomadnetFileDownloadCallback){
|
||||||
|
console.log("did not find nomadnet file download callback for key: " + getNomadnetFileDownloadCallbackKey);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle success
|
||||||
|
if(nomadnetFileDownload.status === "success" && nomadnetFileDownloadCallback.onSuccessCallback){
|
||||||
|
nomadnetFileDownloadCallback.onSuccessCallback(nomadnetFileDownload.file_name, nomadnetFileDownload.file_bytes);
|
||||||
|
delete this.nomadnetFileDownloadCallbacks[getNomadnetFileDownloadCallbackKey];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle failure
|
||||||
|
if(nomadnetFileDownload.status === "failure" && nomadnetFileDownloadCallback.onFailureCallback){
|
||||||
|
nomadnetFileDownloadCallback.onFailureCallback(nomadnetFileDownload.failure_reason);
|
||||||
|
delete this.nomadnetFileDownloadCallbacks[getNomadnetFileDownloadCallbackKey];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle progress
|
||||||
|
if(nomadnetFileDownload.status === "progress" && nomadnetFileDownloadCallback.onProgressCallback){
|
||||||
|
nomadnetFileDownloadCallback.onProgressCallback(nomadnetFileDownload.progress);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDestinationPathClick: function(path) {
|
||||||
|
DialogUtils.alert(`${path.hops} ${ path.hops === 1 ? 'hop' : 'hops' } away via ${path.next_hop_interface}`);
|
||||||
|
},
|
||||||
|
async getNomadnetworkNodeAnnounces() {
|
||||||
|
try {
|
||||||
|
|
||||||
|
// fetch announces for "nomadnetwork.node" aspect
|
||||||
|
const response = await window.axios.get(`/api/v1/announces`, {
|
||||||
|
params: {
|
||||||
|
aspect: "nomadnetwork.node",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// update ui
|
||||||
|
const nodeAnnounces = response.data.announces;
|
||||||
|
for(const nodeAnnounce of nodeAnnounces){
|
||||||
|
this.updateNodeFromAnnounce(nodeAnnounce);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch(e) {
|
||||||
|
// do nothing if failed to load announces
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateNodeFromAnnounce: function(announce) {
|
||||||
|
this.nodes[announce.destination_hash] = {
|
||||||
|
...announce,
|
||||||
|
// helper property for easily grabbing node name from app data
|
||||||
|
name: this.getNodeNameFromAppData(announce.app_data),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getNodeNameFromAppData: function(appData) {
|
||||||
|
try {
|
||||||
|
// app data should be node name, and our server provides it base64 encoded
|
||||||
|
return Utils.decodeBase64ToUtf8String(appData);
|
||||||
|
} catch(e){
|
||||||
|
return "Anonymous Node";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async loadNodePage(destinationHash, pagePath, addToHistory = true, loadFromCache = true) {
|
||||||
|
|
||||||
|
// get new sequence for this page load
|
||||||
|
const seq = ++this.nodePageRequestSequence;
|
||||||
|
|
||||||
|
// get previous page path
|
||||||
|
const previousNodePagePath = this.nodePagePath;
|
||||||
|
|
||||||
|
// update ui
|
||||||
|
this.isLoadingNodePage = true;
|
||||||
|
this.nodePagePath = `${destinationHash}:${pagePath}`;
|
||||||
|
this.nodePageContent = null;
|
||||||
|
this.nodePageProgress = 0;
|
||||||
|
|
||||||
|
// update url bar
|
||||||
|
this.nodePagePathUrlInput = this.nodePagePath;
|
||||||
|
|
||||||
|
// update node path
|
||||||
|
this.getNodePath(destinationHash);
|
||||||
|
|
||||||
|
// add to previous page to history if we are not loading that previous page
|
||||||
|
if(addToHistory && previousNodePagePath != null && previousNodePagePath !== this.nodePagePath){
|
||||||
|
this.nodePagePathHistory.push(previousNodePagePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if we can load this page from the cache
|
||||||
|
if(loadFromCache){
|
||||||
|
|
||||||
|
// load from cache
|
||||||
|
const nodePagePathCacheKey = `${destinationHash}:${pagePath}`;
|
||||||
|
const cachedNodePageContent = this.nodePageCache[nodePagePathCacheKey];
|
||||||
|
|
||||||
|
// if page is cache, we can just return it now
|
||||||
|
if(cachedNodePageContent != null){
|
||||||
|
this.nodePageContent = cachedNodePageContent;
|
||||||
|
this.isLoadingNodePage = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
this.downloadNomadNetPage(destinationHash, pagePath, (pageContent) => {
|
||||||
|
|
||||||
|
// do nothing if callback is for a previous request
|
||||||
|
if(seq !== this.nodePageRequestSequence){
|
||||||
|
console.log("ignoring page content callback for previous page request")
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert micron to html if page ends with .mu extension
|
||||||
|
// otherwise, we will just serve the content as is
|
||||||
|
if(pagePath.endsWith(".mu")){
|
||||||
|
this.nodePageContent = MicronParser.convertMicronToHtml(pageContent);
|
||||||
|
} else {
|
||||||
|
this.nodePageContent = pageContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
// update cache
|
||||||
|
const nodePagePathCacheKey = `${destinationHash}:${pagePath}`;
|
||||||
|
this.nodePageCache[nodePagePathCacheKey] = this.nodePageContent;
|
||||||
|
|
||||||
|
// update page content
|
||||||
|
this.isLoadingNodePage = false;
|
||||||
|
|
||||||
|
// update node path
|
||||||
|
this.getNodePath(destinationHash);
|
||||||
|
|
||||||
|
}, (failureReason) => {
|
||||||
|
|
||||||
|
// do nothing if callback is for a previous request
|
||||||
|
if(seq !== this.nodePageRequestSequence){
|
||||||
|
console.log("ignoring failure callback for previous page request")
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// update page content
|
||||||
|
this.nodePageContent = `Failed loading page: ${failureReason}`;
|
||||||
|
this.isLoadingNodePage = false;
|
||||||
|
|
||||||
|
// update node path
|
||||||
|
this.getNodePath(destinationHash);
|
||||||
|
|
||||||
|
}, (progress) => {
|
||||||
|
|
||||||
|
// do nothing if callback is for a previous request
|
||||||
|
if(seq !== this.nodePageRequestSequence){
|
||||||
|
console.log("ignoring progress callback for previous page request")
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// update page content
|
||||||
|
this.nodePageProgress = Math.round(progress * 100);
|
||||||
|
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async reloadNodePage() {
|
||||||
|
|
||||||
|
// reload current node page without adding to history and without using cache
|
||||||
|
this.onNodePageUrlClick(this.nodePagePath, false, false);
|
||||||
|
|
||||||
|
},
|
||||||
|
async loadPreviousNodePage() {
|
||||||
|
|
||||||
|
// get the previous path from history, or do nothing
|
||||||
|
const previousNodePagePath = this.nodePagePathHistory.pop();
|
||||||
|
if(!previousNodePagePath){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// load the page
|
||||||
|
this.onNodePageUrlClick(previousNodePagePath, false);
|
||||||
|
|
||||||
|
},
|
||||||
|
parseNomadnetworkUrl: function(url) {
|
||||||
|
|
||||||
|
// parse relative urls
|
||||||
|
if(url.startsWith(":")){
|
||||||
|
return {
|
||||||
|
destination_hash: null, // node hash was not in provided url
|
||||||
|
path: url.substring(1), // remove leading ":"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse absolute urls such as 00000000000000000000000000000000:/page/index.mu
|
||||||
|
if(url.includes(":")){
|
||||||
|
|
||||||
|
// parse destination hash and url
|
||||||
|
const [destinationHash, relativeUrl] = url.split(":");
|
||||||
|
|
||||||
|
// ensure destination is expected length
|
||||||
|
if(destinationHash.length === 32){
|
||||||
|
return {
|
||||||
|
destination_hash: destinationHash,
|
||||||
|
path: relativeUrl,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse node id only
|
||||||
|
if(url.length === 32){
|
||||||
|
return {
|
||||||
|
destination_hash: url,
|
||||||
|
path: "/page/index.mu",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// unsupported url
|
||||||
|
return null;
|
||||||
|
|
||||||
|
},
|
||||||
|
onNodePageUrlClick: function(url, addToHistory = true, useCache = true) {
|
||||||
|
|
||||||
|
// open http urls in new tab
|
||||||
|
if(url.startsWith("http://") || url.startsWith("https://")){
|
||||||
|
window.open(url, "_blank");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// lxmf urls should open the conversation
|
||||||
|
if(url.startsWith("lxmf@")){
|
||||||
|
const destinationHash = url.replace("lxmf@", "");
|
||||||
|
if(destinationHash.length === 32){
|
||||||
|
this.openLXMFConversation(destinationHash);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// attempt to parse url
|
||||||
|
const parsedUrl = this.parseNomadnetworkUrl(url);
|
||||||
|
if(parsedUrl != null){
|
||||||
|
|
||||||
|
// use parsed destination hash, or fallback to selected node destination hash
|
||||||
|
const destinationHash = parsedUrl.destination_hash || this.selectedNode.destination_hash;
|
||||||
|
|
||||||
|
// download file
|
||||||
|
if(parsedUrl.path.startsWith("/file/")){
|
||||||
|
|
||||||
|
// prevent simultaneous downloads
|
||||||
|
if(this.isDownloadingNodeFile){
|
||||||
|
DialogUtils.alert("An existing download is in progress. Please wait for it to finish beforing starting another download.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// update ui
|
||||||
|
this.isDownloadingNodeFile = true;
|
||||||
|
this.nodeFilePath = parsedUrl.path.split("/").pop();
|
||||||
|
this.nodeFileProgress = 0;
|
||||||
|
|
||||||
|
// start file download
|
||||||
|
this.downloadNomadNetFile(destinationHash, parsedUrl.path, (fileName, fileBytesBase64) => {
|
||||||
|
|
||||||
|
// no longer downloading
|
||||||
|
this.isDownloadingNodeFile = false;
|
||||||
|
|
||||||
|
// download file to browser
|
||||||
|
this.downloadFileFromBase64(fileName, fileBytesBase64);
|
||||||
|
|
||||||
|
}, (failureReason) => {
|
||||||
|
|
||||||
|
// no longer downloading
|
||||||
|
this.isDownloadingNodeFile = false;
|
||||||
|
|
||||||
|
// show error message
|
||||||
|
DialogUtils.alert(`Failed to download file: ${failureReason}`);
|
||||||
|
|
||||||
|
}, (progress) => {
|
||||||
|
this.nodeFileProgress = Math.round(progress * 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// update selected node, so relative urls work correctly when returned by the new node
|
||||||
|
this.selectedNode = this.nodes[destinationHash] || {
|
||||||
|
name: "Unknown Node",
|
||||||
|
destination_hash: destinationHash,
|
||||||
|
};
|
||||||
|
|
||||||
|
// navigate to node page
|
||||||
|
this.loadNodePage(destinationHash, parsedUrl.path, addToHistory, useCache);
|
||||||
|
return;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// unsupported url
|
||||||
|
DialogUtils.alert("unsupported url: " + url);
|
||||||
|
|
||||||
|
},
|
||||||
|
downloadFileFromBase64: async function(fileName, fileBytesBase64) {
|
||||||
|
|
||||||
|
// create blob from base64 encoded file bytes
|
||||||
|
const byteCharacters = atob(fileBytesBase64);
|
||||||
|
const byteNumbers = new Array(byteCharacters.length);
|
||||||
|
for(let i = 0; i < byteCharacters.length; i++){
|
||||||
|
byteNumbers[i] = byteCharacters.charCodeAt(i);
|
||||||
|
}
|
||||||
|
const byteArray = new Uint8Array(byteNumbers);
|
||||||
|
const blob = new Blob([byteArray]);
|
||||||
|
|
||||||
|
// create object url for blob
|
||||||
|
const objectUrl = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
// create link element to download blob
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = objectUrl;
|
||||||
|
link.download = fileName;
|
||||||
|
link.style.display = "none";
|
||||||
|
document.body.append(link);
|
||||||
|
|
||||||
|
// click link to download file in browser
|
||||||
|
link.click();
|
||||||
|
|
||||||
|
// link element is no longer needed
|
||||||
|
link.remove();
|
||||||
|
|
||||||
|
// revoke object url to clear memory
|
||||||
|
setTimeout(() => URL.revokeObjectURL(objectUrl), 10000);
|
||||||
|
|
||||||
|
},
|
||||||
|
onNodeClick: function(node) {
|
||||||
|
this.selectedNode = node;
|
||||||
|
this.loadNodePage(node.destination_hash, "/page/index.mu");
|
||||||
|
},
|
||||||
|
getNomadnetPageDownloadCallbackKey: function(destinationHash, pagePath) {
|
||||||
|
return `${destinationHash}:${pagePath}`;
|
||||||
|
},
|
||||||
|
getNomadnetFileDownloadCallbackKey: function(destinationHash, filePath) {
|
||||||
|
return `${destinationHash}:${filePath}`;
|
||||||
|
},
|
||||||
|
async getNodePath(destinationHash) {
|
||||||
|
|
||||||
|
// clear previous known path
|
||||||
|
this.selectedNodePath = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
// get path to destination
|
||||||
|
const response = await window.axios.get(`/api/v1/destination/${destinationHash}/path`);
|
||||||
|
|
||||||
|
// update ui
|
||||||
|
this.selectedNodePath = response.data.path;
|
||||||
|
|
||||||
|
} catch(e) {
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
downloadNomadNetFile(destinationHash, filePath, onSuccessCallback, onFailureCallback, onProgressCallback) {
|
||||||
|
try {
|
||||||
|
|
||||||
|
// set callbacks for nomadnet filePath download
|
||||||
|
this.nomadnetFileDownloadCallbacks[this.getNomadnetFileDownloadCallbackKey(destinationHash, filePath)] = {
|
||||||
|
onSuccessCallback: onSuccessCallback,
|
||||||
|
onFailureCallback: onFailureCallback,
|
||||||
|
onProgressCallback: onProgressCallback,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ask reticulum to download file from nomadnet
|
||||||
|
WebSocketConnection.send(JSON.stringify({
|
||||||
|
"type": "nomadnet.file.download",
|
||||||
|
"nomadnet_file_download": {
|
||||||
|
"destination_hash": destinationHash,
|
||||||
|
"file_path": filePath,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
} catch(e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
downloadNomadNetPage(destinationHash, pagePath, onSuccessCallback, onFailureCallback, onProgressCallback) {
|
||||||
|
try {
|
||||||
|
|
||||||
|
// set callbacks for nomadnet page download
|
||||||
|
this.nomadnetPageDownloadCallbacks[this.getNomadnetPageDownloadCallbackKey(destinationHash, pagePath)] = {
|
||||||
|
onSuccessCallback: onSuccessCallback,
|
||||||
|
onFailureCallback: onFailureCallback,
|
||||||
|
onProgressCallback: onProgressCallback,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ask reticulum to download page from nomadnet
|
||||||
|
WebSocketConnection.send(JSON.stringify({
|
||||||
|
"type": "nomadnet.page.download",
|
||||||
|
"nomadnet_page_download": {
|
||||||
|
"destination_hash": destinationHash,
|
||||||
|
"page_path": pagePath,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
} catch(e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,52 +1,54 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="overflow-y-auto space-y-2 p-2">
|
<div class="flex flex-col flex-1 overflow-hidden min-w-full sm:min-w-[500px]">
|
||||||
|
<div class="overflow-y-auto space-y-2 p-2">
|
||||||
|
|
||||||
<!-- failed messages -->
|
<!-- failed messages -->
|
||||||
<div class="bg-white rounded shadow">
|
<div class="bg-white rounded shadow">
|
||||||
<div class="flex border-b border-gray-300 text-gray-700 p-2 font-semibold">Failed Messages</div>
|
<div class="flex border-b border-gray-300 text-gray-700 p-2 font-semibold">Failed Messages</div>
|
||||||
<div class="divide-y text-gray-900">
|
<div class="divide-y text-gray-900">
|
||||||
|
|
||||||
<div class="p-2">
|
<div class="p-2">
|
||||||
<div class="flex items-start">
|
<div class="flex items-start">
|
||||||
<div class="flex items-center h-5">
|
<div class="flex items-center h-5">
|
||||||
<input v-model="config.auto_resend_failed_messages_when_announce_received" @change="onAutoResendFailedMessagesWhenAnnounceReceivedChange" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300">
|
<input v-model="config.auto_resend_failed_messages_when_announce_received" @change="onAutoResendFailedMessagesWhenAnnounceReceivedChange" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300">
|
||||||
|
</div>
|
||||||
|
<label class="ml-2 text-sm font-medium text-gray-900">Auto resend</label>
|
||||||
</div>
|
</div>
|
||||||
<label class="ml-2 text-sm font-medium text-gray-900">Auto resend</label>
|
<div class="text-sm text-gray-700">When enabled, failed messages will auto resend when an announce is received from the intended destination.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-gray-700">When enabled, failed messages will auto resend when an announce is received from the intended destination.</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="p-2">
|
<div class="p-2">
|
||||||
<div class="flex items-start">
|
<div class="flex items-start">
|
||||||
<div class="flex items-center h-5">
|
<div class="flex items-center h-5">
|
||||||
<input v-model="config.allow_auto_resending_failed_messages_with_attachments" @change="onAllowAutoResendingFailedMessagesWithAttachmentsChange" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300">
|
<input v-model="config.allow_auto_resending_failed_messages_with_attachments" @change="onAllowAutoResendingFailedMessagesWithAttachmentsChange" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300">
|
||||||
|
</div>
|
||||||
|
<label class="ml-2 text-sm font-medium text-gray-900">Allow resending with attachments</label>
|
||||||
</div>
|
</div>
|
||||||
<label class="ml-2 text-sm font-medium text-gray-900">Allow resending with attachments</label>
|
<div class="text-sm text-gray-700">When enabled, failed messages that have attachments are allowed to auto resend.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-gray-700">When enabled, failed messages that have attachments are allowed to auto resend.</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- interfaces -->
|
<!-- interfaces -->
|
||||||
<div class="bg-white rounded shadow">
|
<div class="bg-white rounded shadow">
|
||||||
<div class="flex border-b border-gray-300 text-gray-700 p-2 font-semibold">Interfaces</div>
|
<div class="flex border-b border-gray-300 text-gray-700 p-2 font-semibold">Interfaces</div>
|
||||||
<div class="divide-y text-gray-900">
|
<div class="divide-y text-gray-900">
|
||||||
|
|
||||||
<div class="p-2">
|
<div class="p-2">
|
||||||
<div class="flex items-start">
|
<div class="flex items-start">
|
||||||
<div class="flex items-center h-5">
|
<div class="flex items-center h-5">
|
||||||
<input v-model="config.show_suggested_community_interfaces" @change="onShowSuggestedCommunityInterfacesChange" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300">
|
<input v-model="config.show_suggested_community_interfaces" @change="onShowSuggestedCommunityInterfacesChange" type="checkbox" class="w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300">
|
||||||
|
</div>
|
||||||
|
<label class="ml-2 text-sm font-medium text-gray-900">Show Community Interfaces</label>
|
||||||
</div>
|
</div>
|
||||||
<label class="ml-2 text-sm font-medium text-gray-900">Show Community Interfaces</label>
|
<div class="text-sm text-gray-700">When enabled, community interfaces will be shown on the Add Interface page.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-gray-700">When enabled, community interfaces will be shown on the Add Interface page.</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -124,6 +124,13 @@ class Utils {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static decodeBase64ToUtf8String(base64) {
|
||||||
|
// support for decoding base64 as a utf8 string to support emojis and cyrillic characters etc
|
||||||
|
return decodeURIComponent(atob(base64).split('').map(function(c) {
|
||||||
|
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
|
||||||
|
}).join(''));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Utils;
|
export default Utils;
|
||||||
|
|||||||
52
src/frontend/js/WebSocketConnection.js
Normal file
52
src/frontend/js/WebSocketConnection.js
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import mitt from 'mitt';
|
||||||
|
|
||||||
|
class WebSocketConnection {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.emitter = mitt();
|
||||||
|
this.reconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
// add event listener
|
||||||
|
on(event, handler) {
|
||||||
|
this.emitter.on(event, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove event listener
|
||||||
|
off(event, handler) {
|
||||||
|
this.emitter.off(event, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
// emit event
|
||||||
|
emit(type, event) {
|
||||||
|
this.emitter.emit(type, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
reconnect() {
|
||||||
|
|
||||||
|
// connect to websocket
|
||||||
|
this.ws = new WebSocket(location.origin.replace(/^http/, 'ws') + "/ws");
|
||||||
|
|
||||||
|
// auto reconnect when websocket closes
|
||||||
|
this.ws.addEventListener('close', () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.reconnect();
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
// emit data received from websocket
|
||||||
|
this.ws.onmessage = (message) => {
|
||||||
|
this.emit("message", message);
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
send(message) {
|
||||||
|
if(this.ws != null && this.ws.readyState === WebSocket.OPEN){
|
||||||
|
this.ws.send(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new WebSocketConnection();
|
||||||
@@ -6,12 +6,14 @@ import AboutPage from "./components/about/AboutPage.vue";
|
|||||||
import SettingsPage from "./components/settings/SettingsPage.vue";
|
import SettingsPage from "./components/settings/SettingsPage.vue";
|
||||||
import NetworkVisualiserPage from "./components/network/NetworkVisualiserPage.vue";
|
import NetworkVisualiserPage from "./components/network/NetworkVisualiserPage.vue";
|
||||||
import InterfacesPage from "./components/interfaces/InterfacesPage.vue";
|
import InterfacesPage from "./components/interfaces/InterfacesPage.vue";
|
||||||
|
import NomadNetworkPage from "./components/nomadnetwork/NomadNetworkPage.vue";
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHashHistory(),
|
history: createWebHashHistory(),
|
||||||
routes: [
|
routes: [
|
||||||
{ path: '/' },
|
{ path: '/' },
|
||||||
{ path: '/about', name: "about", component: AboutPage },
|
{ path: '/about', name: "about", component: AboutPage },
|
||||||
|
{ path: '/nomadnetwork', name: "nomadnetwork", component: NomadNetworkPage },
|
||||||
{ path: '/settings', name: "settings", component: SettingsPage },
|
{ path: '/settings', name: "settings", component: SettingsPage },
|
||||||
{ path: '/interfaces', name: "interfaces", component: InterfacesPage },
|
{ path: '/interfaces', name: "interfaces", component: InterfacesPage },
|
||||||
{ path: '/network-visualiser', name: "network-visualiser", component: NetworkVisualiserPage },
|
{ path: '/network-visualiser', name: "network-visualiser", component: NetworkVisualiserPage },
|
||||||
|
|||||||
Reference in New Issue
Block a user