@sigx/lynx-dev-client 0.1.0
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/LICENSE +21 -0
- package/android/src/main/kotlin/com/sigx/devclient/DevGenericResourceFetcher.kt +73 -0
- package/android/src/main/kotlin/com/sigx/devclient/DevHomeScreen.kt +163 -0
- package/android/src/main/kotlin/com/sigx/devclient/DevLynxScreen.kt +208 -0
- package/android/src/main/kotlin/com/sigx/devclient/DevMenu.kt +278 -0
- package/android/src/main/kotlin/com/sigx/devclient/DevQRScanner.kt +174 -0
- package/android/src/main/kotlin/com/sigx/devclient/DevSettings.kt +61 -0
- package/android/src/main/kotlin/com/sigx/devclient/DevTemplateProvider.kt +76 -0
- package/android/src/main/kotlin/com/sigx/devclient/DevTemplateResourceFetcher.kt +113 -0
- package/android/src/main/kotlin/com/sigx/devclient/ErrorOverlay.kt +86 -0
- package/android/src/main/kotlin/com/sigx/devclient/PerfHud.kt +100 -0
- package/android/src/main/kotlin/com/sigx/devclient/ShakeDetector.kt +97 -0
- package/android/src/main/kotlin/com/sigx/devclient/SigxDevClient.kt +91 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +11 -0
- package/ios/Sources/SigxDevClient/DevGenericResourceFetcher.swift +47 -0
- package/ios/Sources/SigxDevClient/DevHomeScreen.swift +128 -0
- package/ios/Sources/SigxDevClient/DevMenuView.swift +133 -0
- package/ios/Sources/SigxDevClient/DevQRScanner.swift +165 -0
- package/ios/Sources/SigxDevClient/DevTemplateProvider.swift +68 -0
- package/ios/Sources/SigxDevClient/DevTemplateResourceFetcher.swift +73 -0
- package/ios/Sources/SigxDevClient/ShakeDetector.swift +60 -0
- package/ios/Sources/SigxDevClient/SigxDevClient.swift +106 -0
- package/package.json +30 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
package com.sigx.devclient
|
|
2
|
+
|
|
3
|
+
import androidx.compose.foundation.background
|
|
4
|
+
import androidx.compose.foundation.layout.*
|
|
5
|
+
import androidx.compose.foundation.rememberScrollState
|
|
6
|
+
import androidx.compose.foundation.verticalScroll
|
|
7
|
+
import androidx.compose.material3.Button
|
|
8
|
+
import androidx.compose.material3.ButtonDefaults
|
|
9
|
+
import androidx.compose.material3.Text
|
|
10
|
+
import androidx.compose.runtime.Composable
|
|
11
|
+
import androidx.compose.ui.Modifier
|
|
12
|
+
import androidx.compose.ui.graphics.Color
|
|
13
|
+
import androidx.compose.ui.text.font.FontFamily
|
|
14
|
+
import androidx.compose.ui.text.font.FontWeight
|
|
15
|
+
import androidx.compose.ui.unit.dp
|
|
16
|
+
import androidx.compose.ui.unit.sp
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* React Native-style red screen error overlay.
|
|
20
|
+
* Shows error message and stack trace with dismiss/reload actions.
|
|
21
|
+
*/
|
|
22
|
+
@Composable
|
|
23
|
+
fun ErrorOverlay(
|
|
24
|
+
error: String?,
|
|
25
|
+
onDismiss: () -> Unit,
|
|
26
|
+
onReload: () -> Unit
|
|
27
|
+
) {
|
|
28
|
+
if (error == null) return
|
|
29
|
+
|
|
30
|
+
Box(
|
|
31
|
+
modifier = Modifier
|
|
32
|
+
.fillMaxSize()
|
|
33
|
+
.background(Color(0xFFCC0000))
|
|
34
|
+
) {
|
|
35
|
+
Column(
|
|
36
|
+
modifier = Modifier
|
|
37
|
+
.fillMaxSize()
|
|
38
|
+
.padding(24.dp)
|
|
39
|
+
.verticalScroll(rememberScrollState())
|
|
40
|
+
) {
|
|
41
|
+
Text(
|
|
42
|
+
"Error",
|
|
43
|
+
color = Color.White,
|
|
44
|
+
fontSize = 24.sp,
|
|
45
|
+
fontWeight = FontWeight.Bold
|
|
46
|
+
)
|
|
47
|
+
Spacer(modifier = Modifier.height(16.dp))
|
|
48
|
+
Text(
|
|
49
|
+
error,
|
|
50
|
+
color = Color(0xFFFFCCCC),
|
|
51
|
+
fontSize = 14.sp,
|
|
52
|
+
fontFamily = FontFamily.Monospace,
|
|
53
|
+
lineHeight = 20.sp
|
|
54
|
+
)
|
|
55
|
+
Spacer(modifier = Modifier.height(24.dp))
|
|
56
|
+
Row(
|
|
57
|
+
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
|
58
|
+
) {
|
|
59
|
+
Button(
|
|
60
|
+
onClick = onReload,
|
|
61
|
+
colors = ButtonDefaults.buttonColors(
|
|
62
|
+
containerColor = Color.White,
|
|
63
|
+
contentColor = Color(0xFFCC0000)
|
|
64
|
+
)
|
|
65
|
+
) {
|
|
66
|
+
Text("Reload", fontWeight = FontWeight.Bold)
|
|
67
|
+
}
|
|
68
|
+
Button(
|
|
69
|
+
onClick = onDismiss,
|
|
70
|
+
colors = ButtonDefaults.buttonColors(
|
|
71
|
+
containerColor = Color(0x33FFFFFF),
|
|
72
|
+
contentColor = Color.White
|
|
73
|
+
)
|
|
74
|
+
) {
|
|
75
|
+
Text("Dismiss")
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
Spacer(modifier = Modifier.height(24.dp))
|
|
79
|
+
Text(
|
|
80
|
+
"sigx dev client -- shake device or press Menu to open dev tools",
|
|
81
|
+
color = Color(0x99FFFFFF),
|
|
82
|
+
fontSize = 11.sp
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
package com.sigx.devclient
|
|
2
|
+
|
|
3
|
+
import androidx.compose.foundation.background
|
|
4
|
+
import androidx.compose.foundation.layout.*
|
|
5
|
+
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
6
|
+
import androidx.compose.material3.Text
|
|
7
|
+
import androidx.compose.runtime.*
|
|
8
|
+
import androidx.compose.ui.Modifier
|
|
9
|
+
import androidx.compose.ui.graphics.Color
|
|
10
|
+
import androidx.compose.ui.text.font.FontFamily
|
|
11
|
+
import androidx.compose.ui.unit.dp
|
|
12
|
+
import androidx.compose.ui.unit.sp
|
|
13
|
+
import com.lynx.tasm.LynxPerfMetric
|
|
14
|
+
import com.lynx.tasm.LynxView
|
|
15
|
+
import kotlinx.coroutines.delay
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Translucent performance overlay showing key Lynx rendering metrics.
|
|
19
|
+
* Uses LynxView.forceGetPerf() to read timing data.
|
|
20
|
+
*/
|
|
21
|
+
@Composable
|
|
22
|
+
fun PerfHud(
|
|
23
|
+
visible: Boolean,
|
|
24
|
+
lynxView: LynxView?,
|
|
25
|
+
modifier: Modifier = Modifier
|
|
26
|
+
) {
|
|
27
|
+
if (!visible || lynxView == null) return
|
|
28
|
+
|
|
29
|
+
var metrics by remember { mutableStateOf<LynxPerfMetric?>(null) }
|
|
30
|
+
|
|
31
|
+
LaunchedEffect(lynxView) {
|
|
32
|
+
while (true) {
|
|
33
|
+
try {
|
|
34
|
+
metrics = lynxView.forceGetPerf()
|
|
35
|
+
} catch (_: Exception) {}
|
|
36
|
+
delay(2000)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
Box(
|
|
41
|
+
modifier = modifier
|
|
42
|
+
.padding(8.dp)
|
|
43
|
+
.background(Color(0xCC1E1E1E), RoundedCornerShape(8.dp))
|
|
44
|
+
.padding(10.dp)
|
|
45
|
+
) {
|
|
46
|
+
Column {
|
|
47
|
+
Text(
|
|
48
|
+
"sigx perf",
|
|
49
|
+
color = Color(0xFF7C3AED),
|
|
50
|
+
fontSize = 11.sp,
|
|
51
|
+
fontFamily = FontFamily.Monospace
|
|
52
|
+
)
|
|
53
|
+
Spacer(modifier = Modifier.height(4.dp))
|
|
54
|
+
|
|
55
|
+
metrics?.let { m ->
|
|
56
|
+
PerfLine("TTI", m.tti)
|
|
57
|
+
PerfLine("Layout", m.layout)
|
|
58
|
+
PerfLine("JS Core", m.jsFinishLoadCore)
|
|
59
|
+
PerfLine("JS App", m.jsFinishLoadApp)
|
|
60
|
+
PerfLine("TASM Decode", m.tasmBinaryDecode)
|
|
61
|
+
PerfLine("Render Page", m.renderPage)
|
|
62
|
+
PerfLine("Diff Root", m.diffRootCreate)
|
|
63
|
+
if (m.isHasActualFMP) {
|
|
64
|
+
PerfLine("FMP", m.actualFMPDuration)
|
|
65
|
+
}
|
|
66
|
+
} ?: Text(
|
|
67
|
+
"Waiting for metrics...",
|
|
68
|
+
color = Color(0xFF94A3B8),
|
|
69
|
+
fontSize = 10.sp,
|
|
70
|
+
fontFamily = FontFamily.Monospace
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@Composable
|
|
77
|
+
private fun PerfLine(label: String, value: Double) {
|
|
78
|
+
if (value <= 0) return
|
|
79
|
+
Row(
|
|
80
|
+
modifier = Modifier.fillMaxWidth(),
|
|
81
|
+
horizontalArrangement = Arrangement.SpaceBetween
|
|
82
|
+
) {
|
|
83
|
+
Text(
|
|
84
|
+
label,
|
|
85
|
+
color = Color(0xFF94A3B8),
|
|
86
|
+
fontSize = 10.sp,
|
|
87
|
+
fontFamily = FontFamily.Monospace
|
|
88
|
+
)
|
|
89
|
+
Text(
|
|
90
|
+
"%.1fms".format(value),
|
|
91
|
+
color = when {
|
|
92
|
+
value < 100 -> Color(0xFF22C55E)
|
|
93
|
+
value < 300 -> Color(0xFFEAB308)
|
|
94
|
+
else -> Color(0xFFEF4444)
|
|
95
|
+
},
|
|
96
|
+
fontSize = 10.sp,
|
|
97
|
+
fontFamily = FontFamily.Monospace
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
package com.sigx.devclient
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.hardware.Sensor
|
|
5
|
+
import android.hardware.SensorEvent
|
|
6
|
+
import android.hardware.SensorEventListener
|
|
7
|
+
import android.hardware.SensorManager
|
|
8
|
+
import kotlin.math.sqrt
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Detects device shakes using the accelerometer.
|
|
12
|
+
* Ported from React Native's ShakeDetector
|
|
13
|
+
* (ReactAndroid/.../common/ShakeDetector.java) — requires sustained
|
|
14
|
+
* high-magnitude motion within a short window, not a single spike,
|
|
15
|
+
* so casual handling does not trigger it.
|
|
16
|
+
*/
|
|
17
|
+
class ShakeDetector(
|
|
18
|
+
private val context: Context,
|
|
19
|
+
private val onShake: () -> Unit
|
|
20
|
+
) : SensorEventListener {
|
|
21
|
+
|
|
22
|
+
private var sensorManager: SensorManager? = null
|
|
23
|
+
|
|
24
|
+
private val timestamps = LongArray(MAX_SAMPLES)
|
|
25
|
+
private val magnitudes = FloatArray(MAX_SAMPLES)
|
|
26
|
+
private var currentIndex = 0
|
|
27
|
+
private var numShakes = 0
|
|
28
|
+
private var lastTimestamp = 0L
|
|
29
|
+
|
|
30
|
+
companion object {
|
|
31
|
+
private const val MAX_SAMPLES = 25
|
|
32
|
+
private const val MIN_TIME_BETWEEN_SAMPLES_NS = 20_000_000L // 20 ms
|
|
33
|
+
private const val VISIBLE_TIME_RANGE_NS = 500_000_000L // 500 ms
|
|
34
|
+
private const val ACCELERATION_THRESHOLD = 15f // m/s² (~1.5× gravity)
|
|
35
|
+
private const val MIN_NUM_SHAKES = 1
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
fun start() {
|
|
39
|
+
sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as? SensorManager
|
|
40
|
+
sensorManager?.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)?.let { sensor ->
|
|
41
|
+
sensorManager?.registerListener(this, sensor, SensorManager.SENSOR_DELAY_GAME)
|
|
42
|
+
}
|
|
43
|
+
reset()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
fun stop() {
|
|
47
|
+
sensorManager?.unregisterListener(this)
|
|
48
|
+
sensorManager = null
|
|
49
|
+
reset()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
override fun onSensorChanged(event: SensorEvent?) {
|
|
53
|
+
val e = event ?: return
|
|
54
|
+
if (e.timestamp - lastTimestamp < MIN_TIME_BETWEEN_SAMPLES_NS) return
|
|
55
|
+
lastTimestamp = e.timestamp
|
|
56
|
+
|
|
57
|
+
val ax = e.values[0]
|
|
58
|
+
val ay = e.values[1]
|
|
59
|
+
val az = e.values[2]
|
|
60
|
+
|
|
61
|
+
timestamps[currentIndex] = e.timestamp
|
|
62
|
+
magnitudes[currentIndex] = sqrt(ax * ax + ay * ay + az * az)
|
|
63
|
+
|
|
64
|
+
maybeDispatchShake(e.timestamp)
|
|
65
|
+
currentIndex = (currentIndex + 1) % MAX_SAMPLES
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private fun maybeDispatchShake(now: Long) {
|
|
69
|
+
if (numShakes >= 8 * MIN_NUM_SHAKES) {
|
|
70
|
+
reset()
|
|
71
|
+
onShake()
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
var count = 0
|
|
76
|
+
for (i in 0 until MAX_SAMPLES) {
|
|
77
|
+
val idx = (currentIndex - i + MAX_SAMPLES) % MAX_SAMPLES
|
|
78
|
+
if (now - timestamps[idx] > VISIBLE_TIME_RANGE_NS) break
|
|
79
|
+
if (magnitudes[idx] > ACCELERATION_THRESHOLD) {
|
|
80
|
+
count++
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
numShakes = count
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private fun reset() {
|
|
87
|
+
numShakes = 0
|
|
88
|
+
lastTimestamp = 0L
|
|
89
|
+
currentIndex = 0
|
|
90
|
+
for (i in 0 until MAX_SAMPLES) {
|
|
91
|
+
timestamps[i] = 0L
|
|
92
|
+
magnitudes[i] = 0f
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
|
|
97
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
package com.sigx.devclient
|
|
2
|
+
|
|
3
|
+
import android.app.Application
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.util.Log
|
|
6
|
+
import com.lynx.tasm.LynxBooleanOption
|
|
7
|
+
import com.lynx.tasm.LynxEnv
|
|
8
|
+
import com.lynx.tasm.LynxViewBuilder
|
|
9
|
+
import com.lynx.tasm.service.LynxServiceCenter
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Facade API for sigx-lynx dev client.
|
|
13
|
+
*
|
|
14
|
+
* Templates call these methods in order:
|
|
15
|
+
* 1. `SigxDevClient.registerServices()` -- before LynxEnv.init(), registers devtool service
|
|
16
|
+
* 2. `SigxDevClient.enableDevMode()` -- after LynxEnv.init(), enables debug/devtool/logbox
|
|
17
|
+
* 3. `SigxDevClient.configureForDev(builder, context)` -- in LynxView setup for dev mode
|
|
18
|
+
*/
|
|
19
|
+
object SigxDevClient {
|
|
20
|
+
|
|
21
|
+
private const val TAG = "SigxDevClient"
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Register LynxDevToolService with the service center.
|
|
25
|
+
* Call BEFORE LynxEnv.inst().init() inside a BuildConfig.DEBUG check.
|
|
26
|
+
*/
|
|
27
|
+
fun registerServices() {
|
|
28
|
+
try {
|
|
29
|
+
val devToolServiceClass = Class.forName("com.lynx.service.devtool.LynxDevToolService")
|
|
30
|
+
|
|
31
|
+
// LynxDevToolService.INSTANCE is a Kotlin lazy companion property,
|
|
32
|
+
// accessed via the static getINSTANCE() method (not a field).
|
|
33
|
+
val getInstanceMethod = devToolServiceClass.getMethod("getINSTANCE")
|
|
34
|
+
val instance = getInstanceMethod.invoke(null)
|
|
35
|
+
?: throw IllegalStateException("LynxDevToolService.getINSTANCE() returned null")
|
|
36
|
+
|
|
37
|
+
// Register with LynxServiceCenter
|
|
38
|
+
val center = LynxServiceCenter.inst()
|
|
39
|
+
val registerMethod = center.javaClass.methods.firstOrNull {
|
|
40
|
+
it.name == "registerService" &&
|
|
41
|
+
it.parameterTypes.size == 1 &&
|
|
42
|
+
it.parameterTypes[0].isAssignableFrom(instance.javaClass)
|
|
43
|
+
} ?: center.javaClass.methods.first { it.name == "registerService" }
|
|
44
|
+
|
|
45
|
+
registerMethod.invoke(center, instance)
|
|
46
|
+
Log.i(TAG, "Registered LynxDevToolService")
|
|
47
|
+
|
|
48
|
+
// Set preset values via the concrete class methods
|
|
49
|
+
devToolServiceClass.getMethod("setLynxDebugPresetValue", Boolean::class.java)
|
|
50
|
+
.invoke(instance, true)
|
|
51
|
+
devToolServiceClass.getMethod("setLogBoxPresetValue", Boolean::class.java)
|
|
52
|
+
.invoke(instance, true)
|
|
53
|
+
Log.i(TAG, "Set debug preset values")
|
|
54
|
+
} catch (e: Exception) {
|
|
55
|
+
Log.e(TAG, "Failed to register devtool service: ${e.message}", e)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Enable debug features on LynxEnv.
|
|
61
|
+
* MUST be called AFTER LynxEnv.inst().init() -- calling before init() has
|
|
62
|
+
* no effect because init() resets these flags.
|
|
63
|
+
*/
|
|
64
|
+
fun enableDevMode() {
|
|
65
|
+
LynxEnv.inst().enableLynxDebug(true)
|
|
66
|
+
LynxEnv.inst().enableDevtool(true)
|
|
67
|
+
LynxEnv.inst().enableLogBox(true)
|
|
68
|
+
Log.i(TAG, "Enabled devtool, debug, and logbox on LynxEnv (after init)")
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Convenience method that calls registerServices() + enableDevMode().
|
|
73
|
+
* Only use this if you've already called LynxEnv.inst().init().
|
|
74
|
+
*/
|
|
75
|
+
fun init(app: Application) {
|
|
76
|
+
registerServices()
|
|
77
|
+
enableDevMode()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Configure a LynxViewBuilder for dev mode (HTTP resource fetching + HMR).
|
|
82
|
+
* Call when launching with a dev server URL.
|
|
83
|
+
*/
|
|
84
|
+
fun configureForDev(builder: LynxViewBuilder, context: Context) {
|
|
85
|
+
builder.setTemplateProvider(DevTemplateProvider(context))
|
|
86
|
+
builder.setTemplateResourceFetcher(DevTemplateResourceFetcher(context))
|
|
87
|
+
builder.setEnableGenericResourceFetcher(LynxBooleanOption.TRUE)
|
|
88
|
+
builder.setGenericResourceFetcher(DevGenericResourceFetcher())
|
|
89
|
+
Log.i(TAG, "Configured LynxViewBuilder for dev mode (template provider + resource fetchers)")
|
|
90
|
+
}
|
|
91
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sigx/lynx-dev-client
|
|
3
|
+
*
|
|
4
|
+
* Dev client for sigx-lynx apps. Contains native resource fetchers,
|
|
5
|
+
* template provider, and devtool integration that enable HMR and
|
|
6
|
+
* dev tools in debug builds.
|
|
7
|
+
*
|
|
8
|
+
* The JS side is a no-op — all functionality is in the native code
|
|
9
|
+
* that gets auto-linked during prebuild.
|
|
10
|
+
*/
|
|
11
|
+
export declare const DEV_CLIENT_VERSION = "0.1.0";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sigx/lynx-dev-client
|
|
3
|
+
*
|
|
4
|
+
* Dev client for sigx-lynx apps. Contains native resource fetchers,
|
|
5
|
+
* template provider, and devtool integration that enable HMR and
|
|
6
|
+
* dev tools in debug builds.
|
|
7
|
+
*
|
|
8
|
+
* The JS side is a no-op — all functionality is in the native code
|
|
9
|
+
* that gets auto-linked during prebuild.
|
|
10
|
+
*/
|
|
11
|
+
export const DEV_CLIENT_VERSION = '0.1.0';
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Lynx
|
|
3
|
+
|
|
4
|
+
/// Handles generic resource fetching (JS chunks, CSS, JSON) for LynxView.
|
|
5
|
+
/// Required for HMR hot-update JSON/JS fetches during development.
|
|
6
|
+
class DevGenericResourceFetcher: NSObject, LynxGenericResourceFetcher {
|
|
7
|
+
|
|
8
|
+
private let session: URLSession = {
|
|
9
|
+
let config = URLSessionConfiguration.default
|
|
10
|
+
config.timeoutIntervalForRequest = 30
|
|
11
|
+
config.timeoutIntervalForResource = 60
|
|
12
|
+
return URLSession(configuration: config)
|
|
13
|
+
}()
|
|
14
|
+
|
|
15
|
+
func fetchResource(
|
|
16
|
+
_ request: LynxResourceRequest,
|
|
17
|
+
onComplete callback: @escaping LynxGenericResourceCompletionBlock
|
|
18
|
+
) -> (() -> Void)! {
|
|
19
|
+
guard let url = URL(string: request.url ?? "") else {
|
|
20
|
+
callback(nil, NSError(domain: "com.sigx.devclient", code: 400,
|
|
21
|
+
userInfo: [NSLocalizedDescriptionKey: "Invalid URL: \(request.url ?? "")"]))
|
|
22
|
+
return {}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let task = session.dataTask(with: url) { data, response, error in
|
|
26
|
+
if let error = error {
|
|
27
|
+
callback(nil, error)
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
callback(data, nil)
|
|
31
|
+
}
|
|
32
|
+
task.resume()
|
|
33
|
+
|
|
34
|
+
// Return cancellation block
|
|
35
|
+
return { task.cancel() }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
func fetchResourcePath(
|
|
39
|
+
_ request: LynxResourceRequest,
|
|
40
|
+
onComplete callback: @escaping LynxGenericResourcePathCompletionBlock
|
|
41
|
+
) -> (() -> Void)! {
|
|
42
|
+
// Not used for dev server fetching — return error
|
|
43
|
+
callback(nil, NSError(domain: "com.sigx.devclient", code: 501,
|
|
44
|
+
userInfo: [NSLocalizedDescriptionKey: "fetchResourcePath not implemented"]))
|
|
45
|
+
return {}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
|
|
3
|
+
/// Dev-mode landing screen for sigx-lynx apps that ship without a bundled
|
|
4
|
+
/// `main.lynx.bundle`. Lets the user enter a dev-server URL by hand, scan a
|
|
5
|
+
/// QR code, or pick from recent URLs. The app template renders this when it
|
|
6
|
+
/// has no `--sigx_dev_url` launch arg AND no bundle in `Bundle.main`.
|
|
7
|
+
///
|
|
8
|
+
/// Wraps itself in a `NavigationStack` so it owns the title bar; consumer just
|
|
9
|
+
/// drops `DevHomeScreen { url in /* navigate to LynxView */ }` at the root.
|
|
10
|
+
public struct DevHomeScreen: View {
|
|
11
|
+
let onSelectUrl: (String) -> Void
|
|
12
|
+
|
|
13
|
+
@State private var urlText = ""
|
|
14
|
+
@State private var recentUrls: [String] = []
|
|
15
|
+
@State private var showQRScanner = false
|
|
16
|
+
|
|
17
|
+
public init(onSelectUrl: @escaping (String) -> Void) {
|
|
18
|
+
self.onSelectUrl = onSelectUrl
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public var body: some View {
|
|
22
|
+
// NavigationView (iOS 13+) rather than NavigationStack (iOS 16+) so
|
|
23
|
+
// the dev client works on apps with deployment target 15.0.
|
|
24
|
+
NavigationView {
|
|
25
|
+
VStack(spacing: 0) {
|
|
26
|
+
VStack(spacing: 12) {
|
|
27
|
+
HStack {
|
|
28
|
+
TextField("Dev server URL", text: $urlText)
|
|
29
|
+
.textFieldStyle(.roundedBorder)
|
|
30
|
+
.keyboardType(.URL)
|
|
31
|
+
.autocapitalization(.none)
|
|
32
|
+
.disableAutocorrection(true)
|
|
33
|
+
.submitLabel(.go)
|
|
34
|
+
.onSubmit { connect() }
|
|
35
|
+
|
|
36
|
+
Button(action: { showQRScanner = true }) {
|
|
37
|
+
Image(systemName: "qrcode.viewfinder")
|
|
38
|
+
.font(.title2)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
Button(action: connect) {
|
|
43
|
+
Text("Connect")
|
|
44
|
+
.frame(maxWidth: .infinity)
|
|
45
|
+
}
|
|
46
|
+
.buttonStyle(.borderedProminent)
|
|
47
|
+
.disabled(urlText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
|
|
48
|
+
}
|
|
49
|
+
.padding()
|
|
50
|
+
|
|
51
|
+
Divider()
|
|
52
|
+
|
|
53
|
+
if recentUrls.isEmpty {
|
|
54
|
+
Spacer()
|
|
55
|
+
VStack(spacing: 8) {
|
|
56
|
+
Image(systemName: "antenna.radiowaves.left.and.right")
|
|
57
|
+
.font(.largeTitle)
|
|
58
|
+
.foregroundColor(.secondary)
|
|
59
|
+
Text("Enter a dev server URL or scan a QR code")
|
|
60
|
+
.foregroundColor(.secondary)
|
|
61
|
+
.multilineTextAlignment(.center)
|
|
62
|
+
}
|
|
63
|
+
.padding()
|
|
64
|
+
Spacer()
|
|
65
|
+
} else {
|
|
66
|
+
HStack {
|
|
67
|
+
Text("Recent")
|
|
68
|
+
.font(.headline)
|
|
69
|
+
Spacer()
|
|
70
|
+
Button("Clear") {
|
|
71
|
+
SigxDevClient.clearRecentUrls()
|
|
72
|
+
recentUrls = []
|
|
73
|
+
}
|
|
74
|
+
.font(.subheadline)
|
|
75
|
+
}
|
|
76
|
+
.padding(.horizontal)
|
|
77
|
+
.padding(.top, 12)
|
|
78
|
+
|
|
79
|
+
List {
|
|
80
|
+
ForEach(recentUrls, id: \.self) { url in
|
|
81
|
+
Button(action: {
|
|
82
|
+
urlText = url
|
|
83
|
+
connectTo(url)
|
|
84
|
+
}) {
|
|
85
|
+
Text(url)
|
|
86
|
+
.lineLimit(1)
|
|
87
|
+
.foregroundColor(.primary)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
.onDelete { indexSet in
|
|
91
|
+
for index in indexSet {
|
|
92
|
+
SigxDevClient.removeRecentUrl(recentUrls[index])
|
|
93
|
+
}
|
|
94
|
+
recentUrls = SigxDevClient.recentUrls
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
.listStyle(.plain)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
.navigationTitle("sigx-lynx dev")
|
|
101
|
+
.navigationBarTitleDisplayMode(.inline)
|
|
102
|
+
.sheet(isPresented: $showQRScanner) {
|
|
103
|
+
NavigationView {
|
|
104
|
+
DevQRScanner { code in
|
|
105
|
+
showQRScanner = false
|
|
106
|
+
urlText = code
|
|
107
|
+
connectTo(code)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
.onAppear {
|
|
112
|
+
recentUrls = SigxDevClient.recentUrls
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private func connect() {
|
|
118
|
+
let trimmed = urlText.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
119
|
+
guard !trimmed.isEmpty else { return }
|
|
120
|
+
connectTo(trimmed)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private func connectTo(_ url: String) {
|
|
124
|
+
SigxDevClient.addRecentUrl(url)
|
|
125
|
+
recentUrls = SigxDevClient.recentUrls
|
|
126
|
+
onSelectUrl(url)
|
|
127
|
+
}
|
|
128
|
+
}
|