@tamer4lynx/tamer-dev-client 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -0
- package/android/build.gradle.kts +34 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/kotlin/com/nanofuxion/tamerdevclient/DevClientModule.kt +311 -0
- package/android/src/main/kotlin/com/nanofuxion/tamerdevclient/TamerRelogLogService.kt +141 -0
- package/android/src/main/kotlin/com/nanofuxion/tamerdevclient/nsd/NsdDiscovery.kt +138 -0
- package/android/src/main/kotlin/com/nanofuxion/tamerdevclient/nsd/NsdServiceInfoExtensions.kt +26 -0
- package/android/src/main/kotlin/com/nanofuxion/tamerdevclient/nsd/ResolveService.kt +28 -0
- package/android/templates/DevClientManager.kt +49 -0
- package/android/templates/DevServerPrefs.kt +44 -0
- package/android/templates/PortraitCaptureActivity.kt +5 -0
- package/android/templates/ProjectActivity.kt +112 -0
- package/dist/dev-client.lynx.bundle +0 -0
- package/ios/tamerdevclient/tamerdevclient/Classes/DevClientModule.swift +298 -0
- package/ios/tamerdevclient/tamerdevclient/Classes/TamerRelogLogService.swift +198 -0
- package/ios/tamerdevclient/tamerdevclient.podspec +16 -0
- package/ios/templates/DevClientManager.swift +51 -0
- package/ios/templates/DevLauncherViewController.swift +102 -0
- package/ios/templates/DevTemplateProvider.swift +66 -0
- package/ios/templates/LynxInitProcessor.swift +37 -0
- package/ios/templates/ProjectViewController.swift +105 -0
- package/ios/templates/QRScannerViewController.swift +83 -0
- package/lynx.config.ts +19 -0
- package/package.json +39 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
package com.nanofuxion.tamerdevclient.nsd
|
|
2
|
+
|
|
3
|
+
import android.net.nsd.NsdManager
|
|
4
|
+
import android.net.nsd.NsdServiceInfo
|
|
5
|
+
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
6
|
+
import kotlin.coroutines.resume
|
|
7
|
+
import kotlin.coroutines.resumeWithException
|
|
8
|
+
|
|
9
|
+
@Suppress("DEPRECATION")
|
|
10
|
+
suspend fun NsdManager.resolveServiceCoroutine(serviceInfo: NsdServiceInfo): NsdServiceInfo =
|
|
11
|
+
suspendCancellableCoroutine { cont ->
|
|
12
|
+
resolveService(
|
|
13
|
+
serviceInfo,
|
|
14
|
+
object : NsdManager.ResolveListener {
|
|
15
|
+
override fun onResolveFailed(info: NsdServiceInfo, errorCode: Int) {
|
|
16
|
+
if (cont.isActive) {
|
|
17
|
+
cont.resumeWithException(RuntimeException("Resolve failed: errorCode=$errorCode"))
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
override fun onServiceResolved(resolvedInfo: NsdServiceInfo) {
|
|
22
|
+
if (cont.isActive) {
|
|
23
|
+
cont.resume(resolvedInfo)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
)
|
|
28
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
package {{PACKAGE_NAME}}
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.net.Uri
|
|
5
|
+
import android.os.Handler
|
|
6
|
+
import android.os.Looper
|
|
7
|
+
import okhttp3.OkHttpClient
|
|
8
|
+
import okhttp3.Request
|
|
9
|
+
import okhttp3.Response
|
|
10
|
+
import okhttp3.WebSocket
|
|
11
|
+
import okhttp3.WebSocketListener
|
|
12
|
+
|
|
13
|
+
class DevClientManager(private val context: Context, private val onReload: Runnable) {
|
|
14
|
+
private var webSocket: WebSocket? = null
|
|
15
|
+
private val handler = Handler(Looper.getMainLooper())
|
|
16
|
+
private val client = OkHttpClient.Builder()
|
|
17
|
+
.connectTimeout(5, java.util.concurrent.TimeUnit.SECONDS)
|
|
18
|
+
.readTimeout(0, java.util.concurrent.TimeUnit.SECONDS)
|
|
19
|
+
.build()
|
|
20
|
+
|
|
21
|
+
fun connect() {
|
|
22
|
+
val devUrl = DevServerPrefs.getUrl(context) ?: return
|
|
23
|
+
val uri = Uri.parse(devUrl)
|
|
24
|
+
val scheme = if (uri.scheme == "https") "wss" else "ws"
|
|
25
|
+
val host = uri.host ?: return
|
|
26
|
+
val port = if (uri.port > 0) ":${uri.port}" else ""
|
|
27
|
+
val path = (uri.path ?: "").let { p -> (if (p.endsWith("/")) p else p + "/") + "__hmr" }
|
|
28
|
+
val wsUrl = "$scheme://$host$port$path"
|
|
29
|
+
val request = Request.Builder()
|
|
30
|
+
.url(wsUrl)
|
|
31
|
+
.build()
|
|
32
|
+
webSocket = client.newWebSocket(request, object : WebSocketListener() {
|
|
33
|
+
override fun onMessage(webSocket: WebSocket, text: String) {
|
|
34
|
+
try {
|
|
35
|
+
if (text.contains("\"type\":\"reload\"")) {
|
|
36
|
+
handler.post(onReload)
|
|
37
|
+
}
|
|
38
|
+
} catch (_: Exception) { }
|
|
39
|
+
}
|
|
40
|
+
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { }
|
|
41
|
+
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { }
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
fun disconnect() {
|
|
46
|
+
webSocket?.close(1000, null)
|
|
47
|
+
webSocket = null
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
package {{PACKAGE_NAME}}
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.content.SharedPreferences
|
|
5
|
+
import org.json.JSONArray
|
|
6
|
+
|
|
7
|
+
object DevServerPrefs {
|
|
8
|
+
private const val PREFS = "tamer_dev_server"
|
|
9
|
+
private const val KEY_URL = "dev_server_url"
|
|
10
|
+
private const val KEY_RECENT = "dev_server_recent"
|
|
11
|
+
|
|
12
|
+
fun getUrl(context: Context): String? {
|
|
13
|
+
return prefs(context).getString(KEY_URL, null)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
fun setUrl(context: Context, url: String) {
|
|
17
|
+
prefs(context).edit().putString(KEY_URL, url).apply()
|
|
18
|
+
addRecent(context, url)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
fun getRecentUrls(context: Context): List<String> {
|
|
22
|
+
val json = prefs(context).getString(KEY_RECENT, "[]") ?: "[]"
|
|
23
|
+
return try {
|
|
24
|
+
val arr = JSONArray(json)
|
|
25
|
+
(0 until arr.length()).map { arr.getString(it) }.distinct().take(10)
|
|
26
|
+
} catch (_: Exception) { emptyList() }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
fun addRecent(context: Context, url: String) {
|
|
30
|
+
val current = getRecentUrls(context).filter { it != url }
|
|
31
|
+
val updated = listOf(url) + current
|
|
32
|
+
prefs(context).edit()
|
|
33
|
+
.putString(KEY_RECENT, JSONArray(updated.take(10)).toString())
|
|
34
|
+
.apply()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
fun clear(context: Context) {
|
|
38
|
+
prefs(context).edit().clear().apply()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private fun prefs(context: Context): SharedPreferences {
|
|
42
|
+
return context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
package {{PACKAGE_NAME}}
|
|
2
|
+
|
|
3
|
+
import android.content.Intent
|
|
4
|
+
import android.os.Bundle
|
|
5
|
+
import android.os.Handler
|
|
6
|
+
import android.os.Looper
|
|
7
|
+
import android.view.MotionEvent
|
|
8
|
+
import android.view.inputmethod.InputMethodManager
|
|
9
|
+
import android.widget.EditText
|
|
10
|
+
import androidx.appcompat.app.AppCompatActivity
|
|
11
|
+
import androidx.core.view.WindowCompat
|
|
12
|
+
import androidx.core.view.WindowInsetsControllerCompat
|
|
13
|
+
import com.lynx.tasm.LynxView
|
|
14
|
+
import com.lynx.tasm.LynxViewBuilder
|
|
15
|
+
import {{PACKAGE_NAME}}.DevClientManager
|
|
16
|
+
import {{PACKAGE_NAME}}.generated.GeneratedLynxExtensions
|
|
17
|
+
import {{PACKAGE_NAME}}.generated.GeneratedActivityLifecycle
|
|
18
|
+
|
|
19
|
+
class ProjectActivity : AppCompatActivity() {
|
|
20
|
+
private var lynxView: LynxView? = null
|
|
21
|
+
private var devClientManager: DevClientManager? = null
|
|
22
|
+
private val handler = Handler(Looper.getMainLooper())
|
|
23
|
+
|
|
24
|
+
override fun onCreate(savedInstanceState: Bundle?) {
|
|
25
|
+
super.onCreate(savedInstanceState)
|
|
26
|
+
GeneratedActivityLifecycle.onCreate(intent)
|
|
27
|
+
WindowCompat.setDecorFitsSystemWindows(window, false)
|
|
28
|
+
WindowInsetsControllerCompat(window, window.decorView).isAppearanceLightStatusBars = true
|
|
29
|
+
lynxView = buildLynxView()
|
|
30
|
+
setContentView(lynxView)
|
|
31
|
+
GeneratedActivityLifecycle.onViewAttached(lynxView)
|
|
32
|
+
GeneratedLynxExtensions.onHostViewChanged(lynxView)
|
|
33
|
+
lynxView?.renderTemplateUrl("main.lynx.bundle", "")
|
|
34
|
+
devClientManager = DevClientManager(this) { reloadProjectView() }
|
|
35
|
+
devClientManager?.connect()
|
|
36
|
+
GeneratedActivityLifecycle.onCreateDelayed(handler)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private fun reloadProjectView() {
|
|
40
|
+
GeneratedActivityLifecycle.onViewDetached()
|
|
41
|
+
GeneratedLynxExtensions.onHostViewChanged(null)
|
|
42
|
+
lynxView?.destroy()
|
|
43
|
+
|
|
44
|
+
val nextView = buildLynxView()
|
|
45
|
+
lynxView = nextView
|
|
46
|
+
setContentView(nextView)
|
|
47
|
+
GeneratedActivityLifecycle.onViewAttached(nextView)
|
|
48
|
+
GeneratedLynxExtensions.onHostViewChanged(nextView)
|
|
49
|
+
nextView.renderTemplateUrl("main.lynx.bundle", "")
|
|
50
|
+
GeneratedActivityLifecycle.onCreateDelayed(handler)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
override fun onWindowFocusChanged(hasFocus: Boolean) {
|
|
54
|
+
super.onWindowFocusChanged(hasFocus)
|
|
55
|
+
GeneratedActivityLifecycle.onWindowFocusChanged(hasFocus)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
override fun onResume() {
|
|
59
|
+
super.onResume()
|
|
60
|
+
GeneratedActivityLifecycle.onResume()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
|
|
64
|
+
if (ev.action == MotionEvent.ACTION_DOWN) maybeClearFocusedInput(ev)
|
|
65
|
+
return super.dispatchTouchEvent(ev)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private fun maybeClearFocusedInput(ev: MotionEvent) {
|
|
69
|
+
val focused = currentFocus
|
|
70
|
+
if (focused is EditText) {
|
|
71
|
+
val loc = IntArray(2)
|
|
72
|
+
focused.getLocationOnScreen(loc)
|
|
73
|
+
val x = ev.rawX.toInt()
|
|
74
|
+
val y = ev.rawY.toInt()
|
|
75
|
+
if (x < loc[0] || x > loc[0] + focused.width || y < loc[1] || y > loc[1] + focused.height) {
|
|
76
|
+
focused.clearFocus()
|
|
77
|
+
(getSystemService(INPUT_METHOD_SERVICE) as? InputMethodManager)
|
|
78
|
+
?.hideSoftInputFromWindow(focused.windowToken, 0)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
override fun onNewIntent(intent: Intent) {
|
|
84
|
+
super.onNewIntent(intent)
|
|
85
|
+
setIntent(intent)
|
|
86
|
+
GeneratedActivityLifecycle.onNewIntent(intent)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
@Deprecated("Deprecated in Java")
|
|
90
|
+
override fun onBackPressed() {
|
|
91
|
+
GeneratedActivityLifecycle.onBackPressed { consumed ->
|
|
92
|
+
if (!consumed) {
|
|
93
|
+
runOnUiThread { super.onBackPressed() }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
override fun onDestroy() {
|
|
99
|
+
GeneratedActivityLifecycle.onViewDetached()
|
|
100
|
+
GeneratedLynxExtensions.onHostViewChanged(null)
|
|
101
|
+
lynxView?.destroy()
|
|
102
|
+
lynxView = null
|
|
103
|
+
devClientManager?.disconnect()
|
|
104
|
+
super.onDestroy()
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private fun buildLynxView(): LynxView {
|
|
108
|
+
val viewBuilder = LynxViewBuilder()
|
|
109
|
+
viewBuilder.setTemplateProvider(TemplateProvider(this))
|
|
110
|
+
return viewBuilder.build(this)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Lynx
|
|
3
|
+
import AVFoundation
|
|
4
|
+
|
|
5
|
+
private final class BonjourResolver: NSObject, NetServiceBrowserDelegate, NetServiceDelegate {
|
|
6
|
+
var onServersChanged: (([[String: String]]) -> Void)?
|
|
7
|
+
|
|
8
|
+
private let browser = NetServiceBrowser()
|
|
9
|
+
private var services: [String: NetService] = [:]
|
|
10
|
+
private var resolved: [String: [String: String]] = [:]
|
|
11
|
+
|
|
12
|
+
override init() {
|
|
13
|
+
super.init()
|
|
14
|
+
browser.delegate = self
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
func start() {
|
|
18
|
+
browser.schedule(in: .main, forMode: .common)
|
|
19
|
+
browser.searchForServices(ofType: "_tamer._tcp.", inDomain: "local.")
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
func stop() {
|
|
23
|
+
browser.stop()
|
|
24
|
+
browser.remove(from: .main, forMode: .common)
|
|
25
|
+
services.values.forEach { service in
|
|
26
|
+
service.stop()
|
|
27
|
+
service.remove(from: .main, forMode: .common)
|
|
28
|
+
service.delegate = nil
|
|
29
|
+
}
|
|
30
|
+
services.removeAll()
|
|
31
|
+
resolved.removeAll()
|
|
32
|
+
onServersChanged?([])
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
func netServiceBrowserWillSearch(_ browser: NetServiceBrowser) {
|
|
36
|
+
onServersChanged?(Array(resolved.values))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
func netServiceBrowser(_ browser: NetServiceBrowser, didFind service: NetService, moreComing: Bool) {
|
|
40
|
+
let key = serviceKey(service)
|
|
41
|
+
service.delegate = self
|
|
42
|
+
service.schedule(in: .main, forMode: .common)
|
|
43
|
+
services[key] = service
|
|
44
|
+
service.resolve(withTimeout: 10)
|
|
45
|
+
if !moreComing {
|
|
46
|
+
onServersChanged?(sortedServers())
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
func netServiceBrowser(_ browser: NetServiceBrowser, didRemove service: NetService, moreComing: Bool) {
|
|
51
|
+
let key = serviceKey(service)
|
|
52
|
+
services[key]?.stop()
|
|
53
|
+
services[key]?.delegate = nil
|
|
54
|
+
services.removeValue(forKey: key)
|
|
55
|
+
resolved.removeValue(forKey: key)
|
|
56
|
+
if !moreComing {
|
|
57
|
+
onServersChanged?(sortedServers())
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
func netServiceDidResolveAddress(_ sender: NetService) {
|
|
62
|
+
let key = serviceKey(sender)
|
|
63
|
+
guard let host = sender.hostName?.trimmingCharacters(in: CharacterSet(charactersIn: ".")),
|
|
64
|
+
!host.isEmpty,
|
|
65
|
+
sender.port > 0 else {
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
resolved[key] = [
|
|
69
|
+
"url": "http://\(host):\(sender.port)",
|
|
70
|
+
"name": sender.name
|
|
71
|
+
]
|
|
72
|
+
onServersChanged?(sortedServers())
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
func netService(_ sender: NetService, didNotResolve errorDict: [String : NSNumber]) {
|
|
76
|
+
resolved.removeValue(forKey: serviceKey(sender))
|
|
77
|
+
onServersChanged?(sortedServers())
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private func serviceKey(_ service: NetService) -> String {
|
|
81
|
+
"\(service.name)|\(service.type)|\(service.domain)"
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private func sortedServers() -> [[String: String]] {
|
|
85
|
+
resolved.values.sorted { lhs, rhs in
|
|
86
|
+
(lhs["name"] ?? lhs["url"] ?? "") < (rhs["name"] ?? rhs["url"] ?? "")
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@objcMembers
|
|
92
|
+
public final class DevClientModule: NSObject, LynxModule {
|
|
93
|
+
|
|
94
|
+
// MARK: - LynxModule Protocol
|
|
95
|
+
|
|
96
|
+
@objc public static var name: String { "DevClientModule" }
|
|
97
|
+
|
|
98
|
+
@objc public static var methodLookup: [String: String] {
|
|
99
|
+
[
|
|
100
|
+
"getDevServerUrl": NSStringFromSelector(#selector(getDevServerUrl(_:))),
|
|
101
|
+
"setDevServerUrl": NSStringFromSelector(#selector(setDevServerUrl(_:))),
|
|
102
|
+
"getRecentUrls": NSStringFromSelector(#selector(getRecentUrls(_:))),
|
|
103
|
+
"clearDevServerUrl": NSStringFromSelector(#selector(clearDevServerUrl)),
|
|
104
|
+
"scanQR": NSStringFromSelector(#selector(scanQR)),
|
|
105
|
+
"reloadWithProjectBundle": NSStringFromSelector(#selector(reloadWithProjectBundle)),
|
|
106
|
+
"startDiscovery": NSStringFromSelector(#selector(startDiscovery)),
|
|
107
|
+
"stopDiscovery": NSStringFromSelector(#selector(stopDiscovery)),
|
|
108
|
+
"getDiscoveredServers": NSStringFromSelector(#selector(getDiscoveredServers(_:))),
|
|
109
|
+
"checkServerCompatibility": NSStringFromSelector(#selector(checkServerCompatibility(_:callback:))),
|
|
110
|
+
]
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// MARK: - Static Attachment Points (set by DevLauncherViewController)
|
|
114
|
+
|
|
115
|
+
public static weak var shared: DevClientModule?
|
|
116
|
+
|
|
117
|
+
/// Present a QR scanner; call completion with scanned URL string or nil on cancel.
|
|
118
|
+
public static var presentQRScanner: ((@escaping (String?) -> Void) -> Void)?
|
|
119
|
+
|
|
120
|
+
/// Navigate to the project view controller.
|
|
121
|
+
public static var reloadProjectHandler: (() -> Void)?
|
|
122
|
+
|
|
123
|
+
// MARK: - Instance State
|
|
124
|
+
|
|
125
|
+
private weak var lynxContext: LynxContext?
|
|
126
|
+
private var bonjourResolver: BonjourResolver?
|
|
127
|
+
private var lastDiscovered: [[String: String]] = []
|
|
128
|
+
|
|
129
|
+
// MARK: - Init
|
|
130
|
+
|
|
131
|
+
@objc public required init(param: Any) {
|
|
132
|
+
super.init()
|
|
133
|
+
lynxContext = param as? LynxContext
|
|
134
|
+
DevClientModule.shared = self
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
@objc public override init() {
|
|
138
|
+
super.init()
|
|
139
|
+
DevClientModule.shared = self
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// MARK: - LynxModule Methods
|
|
143
|
+
|
|
144
|
+
@objc func getDevServerUrl(_ callback: LynxCallbackBlock) {
|
|
145
|
+
callback(DevServerPrefs.getUrl() as Any)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
@objc func setDevServerUrl(_ url: String) {
|
|
149
|
+
var normalized = url.trimmingCharacters(in: .whitespaces)
|
|
150
|
+
if normalized.hasSuffix("/main.lynx.bundle") {
|
|
151
|
+
normalized = String(normalized.dropLast("/main.lynx.bundle".count))
|
|
152
|
+
} else if normalized.hasSuffix("main.lynx.bundle") {
|
|
153
|
+
normalized = String(normalized.dropLast("main.lynx.bundle".count))
|
|
154
|
+
.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
|
|
155
|
+
}
|
|
156
|
+
DevServerPrefs.setUrl(normalized)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
@objc func getRecentUrls(_ callback: LynxCallbackBlock) {
|
|
160
|
+
callback(DevServerPrefs.getRecentUrls() as NSArray)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
@objc func clearDevServerUrl() {
|
|
164
|
+
DevServerPrefs.clear()
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
@objc func scanQR() {
|
|
168
|
+
DispatchQueue.main.async { [weak self] in
|
|
169
|
+
guard let self = self else { return }
|
|
170
|
+
guard let present = DevClientModule.presentQRScanner else {
|
|
171
|
+
self.emitScanResult(nil)
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
present { [weak self] result in
|
|
175
|
+
self?.emitScanResult(result)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
@objc func reloadWithProjectBundle() {
|
|
181
|
+
DispatchQueue.main.async {
|
|
182
|
+
DevClientModule.reloadProjectHandler?()
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
@objc func startDiscovery() {
|
|
187
|
+
guard bonjourResolver == nil else { return }
|
|
188
|
+
let resolver = BonjourResolver()
|
|
189
|
+
resolver.onServersChanged = { [weak self] servers in
|
|
190
|
+
self?.emitDiscoveredServers(servers)
|
|
191
|
+
}
|
|
192
|
+
bonjourResolver = resolver
|
|
193
|
+
DispatchQueue.main.async {
|
|
194
|
+
resolver.start()
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
@objc func stopDiscovery() {
|
|
199
|
+
bonjourResolver?.stop()
|
|
200
|
+
bonjourResolver = nil
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
@objc func getDiscoveredServers(_ callback: LynxCallbackBlock) {
|
|
204
|
+
let list = lastDiscovered.map { s -> NSDictionary in
|
|
205
|
+
["url": s["url"] ?? "", "name": s["name"] ?? ""] as NSDictionary
|
|
206
|
+
}
|
|
207
|
+
callback(list as NSArray)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
@objc func checkServerCompatibility(_ baseUrl: String, callback: @escaping LynxCallbackBlock) {
|
|
211
|
+
DispatchQueue.global(qos: .utility).async {
|
|
212
|
+
let trimmed = baseUrl.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
|
|
213
|
+
guard let url = URL(string: trimmed + "/meta.json") else {
|
|
214
|
+
callback([true, []] as NSArray)
|
|
215
|
+
return
|
|
216
|
+
}
|
|
217
|
+
var req = URLRequest(url: url)
|
|
218
|
+
req.timeoutInterval = 5
|
|
219
|
+
URLSession.shared.dataTask(with: req) { data, _, _ in
|
|
220
|
+
guard let data = data,
|
|
221
|
+
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
222
|
+
let modules = json["nativeModules"] as? [[String: Any]] else {
|
|
223
|
+
callback([true, []] as NSArray)
|
|
224
|
+
return
|
|
225
|
+
}
|
|
226
|
+
let required = modules.compactMap { $0["moduleClassName"] as? String }
|
|
227
|
+
callback([true, required] as NSArray)
|
|
228
|
+
}.resume()
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// MARK: - Private
|
|
233
|
+
|
|
234
|
+
private func emitScanResult(_ url: String?) {
|
|
235
|
+
let payload = url.map { "{\"url\":\"\($0)\"}" } ?? "{\"url\":\"\"}"
|
|
236
|
+
sendEvent("devclient:scanResult", payload: payload)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private func emitDiscoveredServers(_ servers: [[String: String]]) {
|
|
240
|
+
lastDiscovered = servers
|
|
241
|
+
let payloadServers = servers.map { server -> [String: Any] in
|
|
242
|
+
[
|
|
243
|
+
"url": server["url"] ?? "",
|
|
244
|
+
"name": server["name"] ?? ""
|
|
245
|
+
]
|
|
246
|
+
}
|
|
247
|
+
let payload = (try? JSONSerialization.data(withJSONObject: ["servers": payloadServers]))
|
|
248
|
+
.flatMap { String(data: $0, encoding: .utf8) } ?? "{\"servers\":[]}"
|
|
249
|
+
sendEvent("devclient:discoveredServers", payload: payload)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private func sendEvent(_ name: String, payload: String) {
|
|
253
|
+
let params: [[String: Any]] = [["payload": payload]]
|
|
254
|
+
if let ctx = lynxContext {
|
|
255
|
+
ctx.sendGlobalEvent(name, withParams: params)
|
|
256
|
+
} else if let ctx = DevClientModule.shared?.lynxContext {
|
|
257
|
+
ctx.sendGlobalEvent(name, withParams: params)
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// MARK: - DevServerPrefs
|
|
263
|
+
|
|
264
|
+
public final class DevServerPrefs {
|
|
265
|
+
private static let suite = "tamer_dev_server"
|
|
266
|
+
private static let keyUrl = "dev_server_url"
|
|
267
|
+
private static let keyRecent = "dev_server_recent"
|
|
268
|
+
private static var defaults: UserDefaults { UserDefaults(suiteName: suite) ?? .standard }
|
|
269
|
+
|
|
270
|
+
public static func getUrl() -> String? {
|
|
271
|
+
defaults.string(forKey: keyUrl)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
public static func setUrl(_ url: String) {
|
|
275
|
+
defaults.set(url, forKey: keyUrl)
|
|
276
|
+
addRecent(url)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
public static func getRecentUrls() -> [String] {
|
|
280
|
+
guard let json = defaults.string(forKey: keyRecent),
|
|
281
|
+
let data = json.data(using: .utf8),
|
|
282
|
+
let arr = try? JSONDecoder().decode([String].self, from: data) else { return [] }
|
|
283
|
+
return Array(arr.prefix(10))
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
public static func addRecent(_ url: String) {
|
|
287
|
+
var current = getRecentUrls().filter { $0 != url }
|
|
288
|
+
current.insert(url, at: 0)
|
|
289
|
+
if let data = try? JSONEncoder().encode(Array(current.prefix(10))),
|
|
290
|
+
let json = String(data: data, encoding: .utf8) {
|
|
291
|
+
defaults.set(json, forKey: keyRecent)
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
public static func clear() {
|
|
296
|
+
defaults.removePersistentDomain(forName: suite)
|
|
297
|
+
}
|
|
298
|
+
}
|