-
No Platypush web services found on the network
+
+
Saved services
+
+
+
+
on
+
:
+
+
+
+
-
-
-
on
-
:
+
+
Scanned services
+
+
+
No Platypush web services found on the network
+
+
+
+
+ on
+ :
+
-
diff --git a/app/src/main/assets/web/js/main.js b/app/src/main/assets/web/js/main.js
index 1cffecf..daef38d 100644
--- a/app/src/main/assets/web/js/main.js
+++ b/app/src/main/assets/web/js/main.js
@@ -4,47 +4,164 @@ new Vue({
return {
loading: false,
splash: false,
- services: {},
- addModalVisible: false,
- addModalHost: undefined,
- addModalPort: 8008,
+ services: {
+ scanned: [],
+ saved: [],
+ },
+ addModal: {
+ visible: false,
+ host: undefined,
+ port: 8008,
+ name: undefined,
+ save: false,
+ },
}
},
+ computed: {
+ servicesByName: function() {
+ return Object.values(this.services).reduce((obj, services) =>
+ services.reduce((obj2, srv) => {
+ obj2[srv.name] = srv
+ return obj2
+ }, obj), {})
+ },
+
+ servicesByHostAndPort: function() {
+ return Object.values(this.services).reduce((obj, services) =>
+ services.reduce((obj2, srv) => {
+ obj2[`${srv.host}:${srv.port}`] = srv
+ return obj2
+ }, obj), {})
+ },
+ },
+
methods: {
refresh: function() {
- this.services = {
- ...this.services,
- ...JSON.parse(app.pollServices()),
+ this.services.scanned = [
+ ...this.services.scanned,
+ ...JSON.parse(app.pollServices()).filter((srv) => !(srv.name in this.servicesByName)),
+ ]
+ },
+
+ onServiceClick: function(service) {
+ this.connect(service.host, service.port)
+ },
+
+ onServiceConnect: function() {
+ if (!this.addModal.host?.length) {
+ app.alert('Please specify a host name or IP address')
+ return
+ }
+
+ if (this.addModal.save) {
+ if (!this.saveService(this.addModal.host, this.addModal.port, this.addModal.name || ''))
+ return
+ }
+
+ this.connect(this.addModal.host, this.addModal.port)
+ },
+
+ saveService: function(host, port, name) {
+ name = this.addModal.name.trim()
+ if (!name.length) {
+ app.alert('Please specify a name')
+ return false
+ }
+
+ let overwrite = false
+ if (name in this.servicesByName) {
+ if (!confirm(`A service named ${name} already exists. ` +
+ `Do you want to overwrite it?`))
+ return false
+
+ overwrite = true
+ } else if (`${host}:${port}` in this.servicesByHostAndPort) {
+ if (!confirm(`A service on ${host}:${port} already exists. ` +
+ `Do you want to overwrite it?`))
+ return false
+
+ overwrite = true
+ }
+
+ this.loading = true
+ try {
+ const rs = app.saveService(host, port, name, overwrite)
+ if (rs?.error)
+ throw rs.error
+
+ this.loadServices()
+ return true
+ } finally {
+ this.loading = false
}
},
- onClick: function(service) {
- this.loadService(service.host, service.port)
+ removeService: function(savedIndex, event) {
+ event.stopPropagation()
+ const srv = this.services.saved[savedIndex]
+ if (!(srv && confirm('Are you sure that you want to remove this service?')))
+ return false
+
+ this.loading = true
+ try {
+ const rs = app.removeService(srv.host, srv.port, srv.name)
+ if (rs?.error)
+ throw rs.error
+
+ this.loadServices()
+ return true
+ } finally {
+ this.loading = false
+ }
},
- onConnect: function() {
- this.loadService(this.addModalHost, this.addModalPort)
- },
-
- loadService: function(host, port) {
+ connect: function(host, port) {
this.loading = true
app.stopServicesPoll()
window.location.href = `http://${host}:${port}/`
},
splashScreen: function(duration) {
- var self = this
+ const self = this
this.splash = true
- window.setTimeout(() => {
+ setTimeout(() => {
self.splash = false
}, duration)
},
+
+ resetAddModal: function() {
+ this.addModal = {
+ host: undefined,
+ port: 8008,
+ name: undefined,
+ save: false,
+ }
+ },
+
+ loadServices: function() {
+ this.services = {
+ ...this.services,
+ ...JSON.parse(app.loadServices())
+ }
+ },
},
mounted: function() {
- this.splashScreen(2500)
+ this.splashScreen(1500)
+
+ this.$watch(() => this.addModal.visible, (newValue) => {
+ if (!newValue)
+ this.resetAddModal()
+ })
+
+ this.$watch(() => this.addModal.save, (newValue) => {
+ if (newValue)
+ this.addModal.name = this.addModal.host
+ })
+
+ this.loadServices()
app.startServicesPoll()
- window.setInterval(this.refresh, 500)
+ setInterval(this.refresh, 500)
}
})
diff --git a/app/src/main/java/tech/platypush/app/JSON.kt b/app/src/main/java/tech/platypush/app/JSON.kt
new file mode 100644
index 0000000..bbd1d32
--- /dev/null
+++ b/app/src/main/java/tech/platypush/app/JSON.kt
@@ -0,0 +1,54 @@
+package tech.platypush.app
+
+import org.json.JSONArray
+import org.json.JSONObject
+import java.util.*
+import kotlin.collections.HashMap
+
+
+class JSON {
+ companion object {
+ private fun toMap(obj: JSONObject): Map
{
+ val ret = HashMap()
+ val keys = obj.keys()
+
+ while (keys.hasNext()) {
+ val key = keys.next()
+ var value = obj[key]
+
+ if (value is JSONObject)
+ value = toMap(value)
+ else if (value is JSONArray)
+ value = toList(value)
+
+ ret[key] = value
+ }
+
+ return ret
+ }
+
+ private fun toList(arr: JSONArray): List {
+ val ret = LinkedList()
+ for (i in 0 until arr.length()) {
+ var value = arr[i]
+
+ if (value is JSONObject)
+ value = toMap(value)
+ else if (value is JSONArray)
+ value = toList(value)
+
+ ret.add(value)
+ }
+
+ return ret
+ }
+
+ fun load(str: String): Map {
+ return toMap(JSONObject(str))
+ }
+
+ fun dump(obj: Map): String {
+ return JSONObject(obj).toString()
+ }
+ }
+}
diff --git a/app/src/main/java/tech/platypush/app/MainActivity.kt b/app/src/main/java/tech/platypush/app/MainActivity.kt
index 0f4c084..96852a8 100644
--- a/app/src/main/java/tech/platypush/app/MainActivity.kt
+++ b/app/src/main/java/tech/platypush/app/MainActivity.kt
@@ -2,6 +2,7 @@ package tech.platypush.app
import android.annotation.SuppressLint
import android.os.Bundle
+import android.webkit.WebChromeClient
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*
@@ -12,6 +13,7 @@ class MainActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
webview.webViewClient = WebView()
+ webview.webChromeClient = WebChromeClient()
webview.settings.javaScriptEnabled = true
webview.settings.javaScriptCanOpenWindowsAutomatically = true
webview.clearCache(true)
diff --git a/app/src/main/java/tech/platypush/app/Services.kt b/app/src/main/java/tech/platypush/app/Services.kt
index 8232201..e3884ce 100644
--- a/app/src/main/java/tech/platypush/app/Services.kt
+++ b/app/src/main/java/tech/platypush/app/Services.kt
@@ -5,8 +5,12 @@ import android.content.Context
import android.net.nsd.NsdManager
import android.net.nsd.NsdServiceInfo
import android.util.Log
+import java.io.File
+import java.io.FileWriter
+import java.lang.RuntimeException
import java.util.*
import java.util.concurrent.locks.ReentrantLock
+import kotlin.concurrent.withLock
/**
@@ -20,6 +24,20 @@ data class Service(val host: String, val port: Int, val name: String?) {
"name" to name
)
}
+
+ override fun toString(): String {
+ return JSON.dump(toMap())
+ }
+
+ companion object {
+ fun load(obj: Map): Service {
+ return Service(
+ host = obj["host"] as String,
+ port = obj["port"] as Int,
+ name = obj["name"] as String?
+ )
+ }
+ }
}
@@ -123,3 +141,109 @@ class Scanner(context: Context) {
})
}
}
+
+/**
+ * Services manager class
+ */
+class Manager(context: Context) {
+ private val servicesFile = File(context.filesDir, "services.json")
+ private val servicesFileLock = ReentrantLock()
+ private val emptyServices = mapOf>(
+ "saved" to emptyList(),
+ "lastConnected" to emptyList()
+ )
+
+ private fun initServicesFile(reset: Boolean = false) {
+ if (reset || !servicesFile.exists())
+ saveServices(emptyServices)
+ }
+
+ fun loadServices(): Map> {
+ initServicesFile()
+ var services: Map> = emptyServices
+
+ try {
+ services = deserializeServices(servicesFile.readText())
+ } catch (e: Exception) {
+ Log.e(TAG, "Error while parsing $servicesFile: resetting it", e)
+ initServicesFile(reset = true)
+ }
+
+ return services
+ }
+
+ private fun saveServices(services: Map>) {
+ servicesFileLock.withLock {
+ FileWriter(servicesFile).use {
+ it.write(serializeServices(services))
+ }
+ }
+ }
+
+ fun saveService(host: String, port: Int, name: String, overwrite: Boolean = false) {
+ val service = Service(host, port, name)
+ val services = this.loadServices().toMutableMap()
+ val savedServices = services["saved"]?.toMutableList() ?: ArrayList()
+ val dupIndex = getSavedServiceIndex(service, savedServices)
+
+ if (dupIndex != null && !overwrite)
+ throw DuplicateServiceException(service)
+
+ savedServices.add(service)
+ services["saved"] = savedServices
+ saveServices(services)
+ }
+
+ fun removeService(host: String? = null, port: Int? = null, name: String? = null) {
+ val service = Service(host=host ?: "", port=port ?: 0, name=name)
+ val services = this.loadServices().toMutableMap()
+ val savedServices = ArrayList(services["saved"] ?: emptyList())
+ val srvIndex = getSavedServiceIndex(service, savedServices)
+ ?: throw NoSuchServiceException(host, port, name)
+
+ savedServices.removeAt(srvIndex)
+ services["saved"] = savedServices
+ saveServices(services)
+ }
+
+ private fun getSavedServiceIndex(service: Service, services: List): Int? {
+ val matchingSrvIndexes = services.indices.filter {
+ (services[it].host == service.host && services[it].port == service.port) ||
+ services[it].name == service.name
+ }
+
+ return if (matchingSrvIndexes.isNotEmpty()) matchingSrvIndexes[0] else null
+ }
+
+ fun serializeServices(services: Map>): String {
+ val parsedServices = HashMap>>()
+
+ for (srvType in listOf("saved", "lastConnected")) {
+ val outputList = LinkedList